skillex 0.2.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/CHANGELOG.md +64 -0
- package/LICENSE +21 -0
- package/README.md +631 -0
- package/bin/skillex.js +11 -0
- package/dist/adapters.d.ts +60 -0
- package/dist/adapters.js +213 -0
- package/dist/catalog.d.ts +73 -0
- package/dist/catalog.js +356 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +885 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +25 -0
- package/dist/confirm.d.ts +8 -0
- package/dist/confirm.js +24 -0
- package/dist/fs.d.ts +79 -0
- package/dist/fs.js +184 -0
- package/dist/http.d.ts +43 -0
- package/dist/http.js +123 -0
- package/dist/install.d.ts +115 -0
- package/dist/install.js +895 -0
- package/dist/output.d.ts +46 -0
- package/dist/output.js +78 -0
- package/dist/runner.d.ts +31 -0
- package/dist/runner.js +94 -0
- package/dist/skill.d.ts +23 -0
- package/dist/skill.js +61 -0
- package/dist/sync.d.ts +41 -0
- package/dist/sync.js +384 -0
- package/dist/types.d.ts +442 -0
- package/dist/types.js +83 -0
- package/dist/ui.d.ts +43 -0
- package/dist/ui.js +78 -0
- package/dist/user-config.d.ts +39 -0
- package/dist/user-config.js +54 -0
- package/package.json +93 -0
package/dist/sync.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { createSymlink, ensureDir, readJson, readSymlink, readText, removePath, writeText } from "./fs.js";
|
|
3
|
+
import { getAdapter } from "./adapters.js";
|
|
4
|
+
import { normalizeSkillContent, parseSkillFrontmatter } from "./skill.js";
|
|
5
|
+
import { SyncError } from "./types.js";
|
|
6
|
+
const MANAGED_START = "<!-- SKILLEX:START -->";
|
|
7
|
+
const MANAGED_END = "<!-- SKILLEX:END -->";
|
|
8
|
+
const AUTO_INJECT_START = "<!-- SKILLEX:AUTO-INJECT:START -->";
|
|
9
|
+
const AUTO_INJECT_END = "<!-- SKILLEX:AUTO-INJECT:END -->";
|
|
10
|
+
const LEGACY_MANAGED_BLOCKS = [{ start: "<!-- ASKILL:START -->", end: "<!-- ASKILL:END -->" }];
|
|
11
|
+
const LEGACY_AUTO_INJECT_BLOCKS = [
|
|
12
|
+
{ start: "<!-- ASKILL:AUTO-INJECT:START -->", end: "<!-- ASKILL:AUTO-INJECT:END -->" },
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Loads installed skill documents from the local workspace state directory.
|
|
16
|
+
*
|
|
17
|
+
* @param context - Workspace root and lockfile context.
|
|
18
|
+
* @returns Installed skill documents used for sync rendering.
|
|
19
|
+
*/
|
|
20
|
+
export async function loadInstalledSkillDocuments(context) {
|
|
21
|
+
const installedEntries = Object.entries(context.lockfile.installed || {}).sort(([left], [right]) => left.localeCompare(right));
|
|
22
|
+
const documents = [];
|
|
23
|
+
for (const [skillId, metadata] of installedEntries) {
|
|
24
|
+
const skillDir = path.resolve(context.cwd, metadata.path);
|
|
25
|
+
const manifest = (await readJson(path.join(skillDir, "skill.json"), {})) || {};
|
|
26
|
+
const entry = manifest.entry || "SKILL.md";
|
|
27
|
+
const rawContent = (await readText(path.join(skillDir, entry), "")) || "";
|
|
28
|
+
const frontmatter = parseSkillFrontmatter(rawContent);
|
|
29
|
+
documents.push({
|
|
30
|
+
id: skillId,
|
|
31
|
+
name: manifest.name || metadata.name || skillId,
|
|
32
|
+
version: manifest.version || metadata.version || "0.1.0",
|
|
33
|
+
body: normalizeSkillContent(rawContent),
|
|
34
|
+
skillDir,
|
|
35
|
+
scripts: manifest.scripts || {},
|
|
36
|
+
autoInject: Boolean(frontmatter.autoInject && frontmatter.activationPrompt),
|
|
37
|
+
activationPrompt: frontmatter.activationPrompt || null,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return documents;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Synchronizes installed skills into the target file consumed by an adapter.
|
|
44
|
+
*
|
|
45
|
+
* @param options - Sync execution options.
|
|
46
|
+
* @returns Final sync result.
|
|
47
|
+
* @throws {SyncError} When sync preparation or file writing fails.
|
|
48
|
+
*/
|
|
49
|
+
export async function syncAdapterFiles(options) {
|
|
50
|
+
try {
|
|
51
|
+
const prepared = await prepareSyncAdapterFiles(options);
|
|
52
|
+
if (!options.dryRun) {
|
|
53
|
+
await ensureDir(path.dirname(prepared.absoluteTargetPath));
|
|
54
|
+
if (prepared.generatedSourcePath) {
|
|
55
|
+
await ensureDir(path.dirname(prepared.generatedSourcePath));
|
|
56
|
+
await writeText(prepared.generatedSourcePath, prepared.nextContent);
|
|
57
|
+
}
|
|
58
|
+
if (prepared.syncMode === "symlink" && prepared.generatedSourcePath) {
|
|
59
|
+
const createLink = options.linkFactory || createSymlink;
|
|
60
|
+
const linkResult = await createLink(prepared.generatedSourcePath, prepared.absoluteTargetPath);
|
|
61
|
+
if (linkResult.fallback) {
|
|
62
|
+
(options.warn || console.error)(`Aviso: symlink indisponivel para ${prepared.targetPath}; usando copia no lugar.`);
|
|
63
|
+
await writeText(prepared.absoluteTargetPath, prepared.nextContent);
|
|
64
|
+
await removePath(prepared.generatedSourcePath);
|
|
65
|
+
prepared.syncMode = "copy";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
await writeText(prepared.absoluteTargetPath, prepared.nextContent);
|
|
70
|
+
}
|
|
71
|
+
for (const cleanupPath of prepared.cleanupPaths) {
|
|
72
|
+
await removePath(cleanupPath);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
adapter: prepared.adapter,
|
|
77
|
+
targetPath: prepared.targetPath,
|
|
78
|
+
changed: prepared.changed,
|
|
79
|
+
diff: prepared.diff,
|
|
80
|
+
syncMode: prepared.syncMode,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof SyncError) {
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
+
throw new SyncError(`Falha ao sincronizar adapter ${options.adapterId}: ${message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Prepares the next sync state without writing files.
|
|
93
|
+
*
|
|
94
|
+
* @param options - Sync preparation options.
|
|
95
|
+
* @returns Prepared sync state with before/after content and diff.
|
|
96
|
+
* @throws {SyncError} When the adapter cannot be prepared.
|
|
97
|
+
*/
|
|
98
|
+
export async function prepareSyncAdapterFiles(options) {
|
|
99
|
+
const adapter = getAdapter(options.adapterId);
|
|
100
|
+
const targetPath = path.join(options.cwd, adapter.syncTarget);
|
|
101
|
+
const relativeTargetPath = toPosix(path.relative(options.cwd, targetPath));
|
|
102
|
+
const body = renderInstalledSkills(options.skills);
|
|
103
|
+
const autoInjectBlock = buildAutoInjectBlock(options.skills);
|
|
104
|
+
const cleanupPaths = (adapter.legacySyncTargets || [])
|
|
105
|
+
.map((relativePath) => path.join(options.cwd, relativePath))
|
|
106
|
+
.filter((absolutePath) => absolutePath !== targetPath);
|
|
107
|
+
if (adapter.syncMode === "managed-block") {
|
|
108
|
+
const existing = (await readText(targetPath, "")) || "";
|
|
109
|
+
const nextManaged = upsertManagedBlock(existing, wrapManagedBlock(MANAGED_START, MANAGED_END, body));
|
|
110
|
+
const nextContent = upsertAutoInjectBlock(nextManaged, autoInjectBlock);
|
|
111
|
+
return {
|
|
112
|
+
adapter: adapter.id,
|
|
113
|
+
absoluteTargetPath: targetPath,
|
|
114
|
+
targetPath: relativeTargetPath,
|
|
115
|
+
cleanupPaths,
|
|
116
|
+
changed: normalizeComparableText(existing) !== normalizeComparableText(nextContent),
|
|
117
|
+
currentContent: existing,
|
|
118
|
+
nextContent,
|
|
119
|
+
diff: createTextDiff(existing, nextContent, relativeTargetPath),
|
|
120
|
+
syncMode: "copy",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const nextContent = buildManagedFileContent(adapter.id, body, autoInjectBlock);
|
|
124
|
+
const requestedMode = options.mode || "symlink";
|
|
125
|
+
if (requestedMode === "copy") {
|
|
126
|
+
const existing = (await readText(targetPath, "")) || "";
|
|
127
|
+
return {
|
|
128
|
+
adapter: adapter.id,
|
|
129
|
+
absoluteTargetPath: targetPath,
|
|
130
|
+
targetPath: relativeTargetPath,
|
|
131
|
+
cleanupPaths,
|
|
132
|
+
changed: normalizeComparableText(existing) !== normalizeComparableText(nextContent),
|
|
133
|
+
currentContent: existing,
|
|
134
|
+
nextContent,
|
|
135
|
+
diff: createTextDiff(existing, nextContent, relativeTargetPath),
|
|
136
|
+
syncMode: "copy",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const generatedSourcePath = path.join(options.statePaths.generatedDirPath, adapter.id, path.basename(adapter.syncTarget));
|
|
140
|
+
const currentDescriptor = await describeTarget(targetPath);
|
|
141
|
+
const currentVisibleContent = (await readText(targetPath, "")) || "";
|
|
142
|
+
const nextDescriptor = `symlink -> ${toPosix(path.relative(path.dirname(targetPath), generatedSourcePath))}\n`;
|
|
143
|
+
const descriptorChanged = normalizeComparableText(currentDescriptor) !== normalizeComparableText(nextDescriptor);
|
|
144
|
+
const contentChanged = normalizeComparableText(currentVisibleContent) !== normalizeComparableText(nextContent);
|
|
145
|
+
return {
|
|
146
|
+
adapter: adapter.id,
|
|
147
|
+
absoluteTargetPath: targetPath,
|
|
148
|
+
targetPath: relativeTargetPath,
|
|
149
|
+
cleanupPaths,
|
|
150
|
+
changed: descriptorChanged || contentChanged,
|
|
151
|
+
currentContent: currentDescriptor,
|
|
152
|
+
nextContent,
|
|
153
|
+
diff: createManagedFileDiff({
|
|
154
|
+
targetPath: relativeTargetPath,
|
|
155
|
+
currentDescriptor,
|
|
156
|
+
nextDescriptor,
|
|
157
|
+
generatedPath: toPosix(path.relative(options.cwd, generatedSourcePath)),
|
|
158
|
+
currentContent: currentVisibleContent,
|
|
159
|
+
nextContent,
|
|
160
|
+
}),
|
|
161
|
+
syncMode: "symlink",
|
|
162
|
+
generatedSourcePath,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Renders installed skills into a single markdown document.
|
|
167
|
+
*
|
|
168
|
+
* @param skills - Installed skill documents.
|
|
169
|
+
* @returns Consolidated markdown body.
|
|
170
|
+
*/
|
|
171
|
+
export function renderInstalledSkills(skills) {
|
|
172
|
+
const sections = skills.map(renderSkillSection).filter(Boolean);
|
|
173
|
+
const lines = [
|
|
174
|
+
"## Skillex Managed Skills",
|
|
175
|
+
"",
|
|
176
|
+
"> Conteudo gerado por `skillex sync`. Edicoes aqui podem ser sobrescritas.",
|
|
177
|
+
"",
|
|
178
|
+
];
|
|
179
|
+
if (sections.length === 0) {
|
|
180
|
+
lines.push("Nenhuma skill instalada no momento.");
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
lines.push(sections.join("\n\n---\n\n"));
|
|
184
|
+
}
|
|
185
|
+
return `${lines.join("\n").trim()}\n`;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Builds the managed auto-inject block for all installed skills that request it.
|
|
189
|
+
*
|
|
190
|
+
* @param skills - Installed skill documents.
|
|
191
|
+
* @returns Managed auto-inject block or `null` when nothing should be injected.
|
|
192
|
+
*/
|
|
193
|
+
export function buildAutoInjectBlock(skills) {
|
|
194
|
+
const entries = skills
|
|
195
|
+
.filter((skill) => skill.autoInject && skill.activationPrompt)
|
|
196
|
+
.map((skill) => [`### ${skill.name} (\`${skill.id}\`)`, "", skill.activationPrompt].join("\n"));
|
|
197
|
+
if (entries.length === 0) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const body = [
|
|
201
|
+
"## Skillex Auto-Inject",
|
|
202
|
+
"",
|
|
203
|
+
"> Prompts de ativacao gerados automaticamente por `skillex sync`.",
|
|
204
|
+
"",
|
|
205
|
+
entries.join("\n\n---\n\n"),
|
|
206
|
+
].join("\n");
|
|
207
|
+
return wrapManagedBlock(AUTO_INJECT_START, AUTO_INJECT_END, body);
|
|
208
|
+
}
|
|
209
|
+
function renderSkillSection(skill) {
|
|
210
|
+
const body = skill.body.trim() || "_Sem conteudo._";
|
|
211
|
+
return [`### ${skill.name} (\`${skill.id}@${skill.version}\`)`, "", body].join("\n");
|
|
212
|
+
}
|
|
213
|
+
function buildManagedFileContent(adapterId, body, autoInjectBlock) {
|
|
214
|
+
const sections = [body.trim()];
|
|
215
|
+
if (autoInjectBlock) {
|
|
216
|
+
sections.push(autoInjectBlock.trim());
|
|
217
|
+
}
|
|
218
|
+
switch (adapterId) {
|
|
219
|
+
case "cursor":
|
|
220
|
+
return [
|
|
221
|
+
"---",
|
|
222
|
+
'description: "Skillex managed skills"',
|
|
223
|
+
"alwaysApply: true",
|
|
224
|
+
"---",
|
|
225
|
+
"",
|
|
226
|
+
sections.join("\n\n"),
|
|
227
|
+
"",
|
|
228
|
+
].join("\n");
|
|
229
|
+
case "windsurf":
|
|
230
|
+
return [
|
|
231
|
+
"---",
|
|
232
|
+
'description: "Skillex managed skills"',
|
|
233
|
+
"trigger: always_on",
|
|
234
|
+
"---",
|
|
235
|
+
"",
|
|
236
|
+
sections.join("\n\n"),
|
|
237
|
+
"",
|
|
238
|
+
].join("\n");
|
|
239
|
+
case "cline":
|
|
240
|
+
return `${sections.join("\n\n")}\n`;
|
|
241
|
+
default:
|
|
242
|
+
throw new SyncError(`Adapter desconhecido: ${adapterId}`, "SYNC_ADAPTER_UNKNOWN");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function wrapManagedBlock(start, end, body) {
|
|
246
|
+
return [start, body.trim(), end, ""].join("\n");
|
|
247
|
+
}
|
|
248
|
+
function upsertManagedBlock(existingContent, blockContent) {
|
|
249
|
+
return upsertNamedBlock(existingContent, blockContent, MANAGED_START, MANAGED_END, LEGACY_MANAGED_BLOCKS);
|
|
250
|
+
}
|
|
251
|
+
function upsertAutoInjectBlock(existingContent, autoInjectBlock) {
|
|
252
|
+
return upsertNamedBlock(existingContent, autoInjectBlock, AUTO_INJECT_START, AUTO_INJECT_END, LEGACY_AUTO_INJECT_BLOCKS);
|
|
253
|
+
}
|
|
254
|
+
function upsertNamedBlock(existingContent, blockContent, start, end, legacyBlocks) {
|
|
255
|
+
const allBlocks = [{ start, end }, ...legacyBlocks];
|
|
256
|
+
let nextContent = existingContent;
|
|
257
|
+
for (const block of allBlocks) {
|
|
258
|
+
const pattern = new RegExp(`${escapeRegExp(block.start)}[\\s\\S]*?${escapeRegExp(block.end)}\\n?`, "m");
|
|
259
|
+
if (pattern.test(nextContent)) {
|
|
260
|
+
nextContent = blockContent ? nextContent.replace(pattern, `${blockContent}\n`) : nextContent.replace(pattern, "");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!blockContent) {
|
|
264
|
+
return nextContent.trimEnd() ? `${nextContent.trimEnd()}\n` : "";
|
|
265
|
+
}
|
|
266
|
+
if (!nextContent.trim()) {
|
|
267
|
+
return `${blockContent}\n`;
|
|
268
|
+
}
|
|
269
|
+
if (nextContent.includes(start)) {
|
|
270
|
+
return nextContent;
|
|
271
|
+
}
|
|
272
|
+
return `${nextContent.trimEnd()}\n\n${blockContent}\n`;
|
|
273
|
+
}
|
|
274
|
+
async function describeTarget(targetPath) {
|
|
275
|
+
const linkTarget = await readSymlink(targetPath);
|
|
276
|
+
if (linkTarget) {
|
|
277
|
+
return `symlink -> ${toPosix(linkTarget)}\n`;
|
|
278
|
+
}
|
|
279
|
+
if (!(await readText(targetPath, null))) {
|
|
280
|
+
return "";
|
|
281
|
+
}
|
|
282
|
+
return "file\n";
|
|
283
|
+
}
|
|
284
|
+
function createManagedFileDiff(context) {
|
|
285
|
+
const descriptorChanged = normalizeComparableText(context.currentDescriptor) !== normalizeComparableText(context.nextDescriptor);
|
|
286
|
+
const contentChanged = normalizeComparableText(context.currentContent) !== normalizeComparableText(context.nextContent);
|
|
287
|
+
if (!descriptorChanged && !contentChanged) {
|
|
288
|
+
return `Sem alteracoes em ${context.targetPath}.\n`;
|
|
289
|
+
}
|
|
290
|
+
const parts = [];
|
|
291
|
+
if (descriptorChanged) {
|
|
292
|
+
parts.push(createTextDiff(context.currentDescriptor, context.nextDescriptor, context.targetPath).trimEnd());
|
|
293
|
+
}
|
|
294
|
+
if (contentChanged) {
|
|
295
|
+
parts.push(createTextDiff(context.currentContent, context.nextContent, context.generatedPath).trimEnd());
|
|
296
|
+
}
|
|
297
|
+
return `${parts.join("\n")}\n`;
|
|
298
|
+
}
|
|
299
|
+
function createTextDiff(currentContent, nextContent, targetPath) {
|
|
300
|
+
if (normalizeComparableText(currentContent) === normalizeComparableText(nextContent)) {
|
|
301
|
+
return `Sem alteracoes em ${targetPath}.\n`;
|
|
302
|
+
}
|
|
303
|
+
const currentLines = splitLines(currentContent);
|
|
304
|
+
const nextLines = splitLines(nextContent);
|
|
305
|
+
const operations = diffLines(currentLines, nextLines);
|
|
306
|
+
const output = [`--- atual/${targetPath}`, `+++ novo/${targetPath}`];
|
|
307
|
+
for (const operation of operations) {
|
|
308
|
+
output.push(`${operation.type}${operation.line}`);
|
|
309
|
+
}
|
|
310
|
+
return `${output.join("\n")}\n`;
|
|
311
|
+
}
|
|
312
|
+
function splitLines(content) {
|
|
313
|
+
if (!content) {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
const lines = content.split("\n");
|
|
317
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
318
|
+
lines.pop();
|
|
319
|
+
}
|
|
320
|
+
return lines;
|
|
321
|
+
}
|
|
322
|
+
function normalizeComparableText(content) {
|
|
323
|
+
if (!content) {
|
|
324
|
+
return "";
|
|
325
|
+
}
|
|
326
|
+
return content.replace(/\r\n/g, "\n").replace(/\n+$/, "\n");
|
|
327
|
+
}
|
|
328
|
+
function diffLines(leftLines, rightLines) {
|
|
329
|
+
const rowCount = leftLines.length + 1;
|
|
330
|
+
const columnCount = rightLines.length + 1;
|
|
331
|
+
const matrix = Array.from({ length: rowCount }, () => Array(columnCount).fill(0));
|
|
332
|
+
for (let leftIndex = leftLines.length - 1; leftIndex >= 0; leftIndex -= 1) {
|
|
333
|
+
for (let rightIndex = rightLines.length - 1; rightIndex >= 0; rightIndex -= 1) {
|
|
334
|
+
const leftLine = leftLines[leftIndex];
|
|
335
|
+
const rightLine = rightLines[rightIndex];
|
|
336
|
+
const currentRow = matrix[leftIndex];
|
|
337
|
+
const nextRow = matrix[leftIndex + 1];
|
|
338
|
+
if (leftLine === rightLine) {
|
|
339
|
+
currentRow[rightIndex] = (nextRow[rightIndex + 1] ?? 0) + 1;
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
currentRow[rightIndex] = Math.max(nextRow[rightIndex] ?? 0, currentRow[rightIndex + 1] ?? 0);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const operations = [];
|
|
347
|
+
let leftIndex = 0;
|
|
348
|
+
let rightIndex = 0;
|
|
349
|
+
while (leftIndex < leftLines.length && rightIndex < rightLines.length) {
|
|
350
|
+
const leftLine = leftLines[leftIndex];
|
|
351
|
+
const rightLine = rightLines[rightIndex];
|
|
352
|
+
if (leftLine === rightLine) {
|
|
353
|
+
operations.push({ type: " ", line: leftLine });
|
|
354
|
+
leftIndex += 1;
|
|
355
|
+
rightIndex += 1;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const currentRow = matrix[leftIndex];
|
|
359
|
+
const nextRow = matrix[leftIndex + 1];
|
|
360
|
+
if ((nextRow[rightIndex] ?? 0) >= (currentRow[rightIndex + 1] ?? 0)) {
|
|
361
|
+
operations.push({ type: "-", line: leftLine });
|
|
362
|
+
leftIndex += 1;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
operations.push({ type: "+", line: rightLine });
|
|
366
|
+
rightIndex += 1;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
while (leftIndex < leftLines.length) {
|
|
370
|
+
operations.push({ type: "-", line: leftLines[leftIndex] });
|
|
371
|
+
leftIndex += 1;
|
|
372
|
+
}
|
|
373
|
+
while (rightIndex < rightLines.length) {
|
|
374
|
+
operations.push({ type: "+", line: rightLines[rightIndex] });
|
|
375
|
+
rightIndex += 1;
|
|
376
|
+
}
|
|
377
|
+
return operations;
|
|
378
|
+
}
|
|
379
|
+
function escapeRegExp(value) {
|
|
380
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
381
|
+
}
|
|
382
|
+
function toPosix(value) {
|
|
383
|
+
return value.split(path.sep).join("/");
|
|
384
|
+
}
|