@3lineas/grooveskills 1.6.0 → 1.7.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.
- package/README.md +1 -0
- package/dist/{chunk-HUG3MG3P.js → chunk-SP3ESDM5.js} +776 -230
- package/dist/chunk-SP3ESDM5.js.map +1 -0
- package/dist/cli.js +42 -3
- package/dist/cli.js.map +1 -1
- package/dist/main.js +1 -1
- package/package.json +3 -2
- package/skills/best-practices/SKILL.md +12 -7
- package/skills/docs-standards/SKILL.md +113 -0
- package/skills/docs-standards/references/adr-template-and-lifecycle.md +111 -0
- package/skills/docs-standards/references/code-comments-and-docstrings.md +147 -0
- package/skills/docs-standards/references/relative-paths-and-diagrams.md +123 -0
- package/dist/chunk-HUG3MG3P.js.map +0 -1
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/main.ts
|
|
2
|
+
import { basename as basename3 } from "path";
|
|
3
|
+
import { userInfo as userInfo2 } from "os";
|
|
4
|
+
import picocolors5 from "picocolors";
|
|
5
|
+
|
|
1
6
|
// src/domains/detector/project-detector.ts
|
|
2
7
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
3
8
|
import { join as join2 } from "path";
|
|
@@ -37,11 +42,11 @@ function detectDependencies(targetPath) {
|
|
|
37
42
|
try {
|
|
38
43
|
const raw = readFileSync(packageJsonPath, "utf-8");
|
|
39
44
|
const parsed = JSON.parse(raw);
|
|
40
|
-
for (const
|
|
41
|
-
detected.add(
|
|
45
|
+
for (const dependencyName of Object.keys(parsed.dependencies ?? {})) {
|
|
46
|
+
detected.add(dependencyName);
|
|
42
47
|
}
|
|
43
|
-
for (const
|
|
44
|
-
detected.add(
|
|
48
|
+
for (const dependencyName of Object.keys(parsed.devDependencies ?? {})) {
|
|
49
|
+
detected.add(dependencyName);
|
|
45
50
|
}
|
|
46
51
|
} catch {
|
|
47
52
|
}
|
|
@@ -94,6 +99,13 @@ var SKILL_CATALOG = [
|
|
|
94
99
|
dependencies: [],
|
|
95
100
|
directoryName: "ux-writing"
|
|
96
101
|
},
|
|
102
|
+
{
|
|
103
|
+
id: "docs-standards",
|
|
104
|
+
name: "Documentation & ADR Standards",
|
|
105
|
+
description: "English documentation standard, relative POSIX links, resilient Mermaid diagrams, ADR lifecycle, and intent-focused docstrings",
|
|
106
|
+
dependencies: [],
|
|
107
|
+
directoryName: "docs-standards"
|
|
108
|
+
},
|
|
97
109
|
{
|
|
98
110
|
id: "typescript-clean",
|
|
99
111
|
name: "TypeScript Clean Code (TS 5.5+)",
|
|
@@ -243,6 +255,7 @@ function detectProjectContext(targetPath) {
|
|
|
243
255
|
"odd-ai",
|
|
244
256
|
"best-practices",
|
|
245
257
|
"ux-writing",
|
|
258
|
+
"docs-standards",
|
|
246
259
|
"error-handling-logging",
|
|
247
260
|
"clean-architecture-ddd",
|
|
248
261
|
"external-service-adapters",
|
|
@@ -257,7 +270,9 @@ function detectProjectContext(targetPath) {
|
|
|
257
270
|
if (skill.dependencies.length === 0) {
|
|
258
271
|
continue;
|
|
259
272
|
}
|
|
260
|
-
const hasMatch = skill.dependencies.some(
|
|
273
|
+
const hasMatch = skill.dependencies.some(
|
|
274
|
+
(dependencyName) => dependencies.includes(dependencyName)
|
|
275
|
+
);
|
|
261
276
|
if (hasMatch && !suggestedSkillIds.includes(skill.id)) {
|
|
262
277
|
suggestedSkillIds.push(skill.id);
|
|
263
278
|
}
|
|
@@ -276,21 +291,21 @@ function detectProjectContext(targetPath) {
|
|
|
276
291
|
}
|
|
277
292
|
|
|
278
293
|
// src/domains/ui/ui-orchestrator.ts
|
|
279
|
-
import * as
|
|
280
|
-
import
|
|
294
|
+
import * as prompts6 from "@clack/prompts";
|
|
295
|
+
import picocolors4 from "picocolors";
|
|
281
296
|
|
|
282
297
|
// src/domains/ui/banners.ts
|
|
283
|
-
import
|
|
298
|
+
import picocolors from "picocolors";
|
|
284
299
|
function renderBanner() {
|
|
285
|
-
const title =
|
|
286
|
-
const badge =
|
|
287
|
-
const subtitle =
|
|
300
|
+
const title = picocolors.bold(picocolors.white(" GROOVE SKILLS "));
|
|
301
|
+
const badge = picocolors.bgMagenta(picocolors.black(title));
|
|
302
|
+
const subtitle = picocolors.dim("Multi-Agent AI Skills & Architectural Standards");
|
|
288
303
|
return `${badge} ${subtitle}
|
|
289
304
|
`;
|
|
290
305
|
}
|
|
291
306
|
|
|
292
307
|
// src/domains/ui/prompt-skills.ts
|
|
293
|
-
import * as
|
|
308
|
+
import * as prompts from "@clack/prompts";
|
|
294
309
|
async function promptSkills(suggestedSkillIds) {
|
|
295
310
|
const selectableSkills = SKILL_CATALOG.filter((skill) => !skill.isCoreHarness);
|
|
296
311
|
const options = selectableSkills.map((skill) => {
|
|
@@ -301,7 +316,7 @@ async function promptSkills(suggestedSkillIds) {
|
|
|
301
316
|
hint: isSuggested ? "Suggested based on your project stack" : skill.description
|
|
302
317
|
};
|
|
303
318
|
});
|
|
304
|
-
const selected = await
|
|
319
|
+
const selected = await prompts.multiselect({
|
|
305
320
|
message: "Which skills would you like to install in this project?",
|
|
306
321
|
options,
|
|
307
322
|
initialValues: suggestedSkillIds,
|
|
@@ -311,7 +326,7 @@ async function promptSkills(suggestedSkillIds) {
|
|
|
311
326
|
}
|
|
312
327
|
|
|
313
328
|
// src/domains/ui/prompt-agents.ts
|
|
314
|
-
import * as
|
|
329
|
+
import * as prompts2 from "@clack/prompts";
|
|
315
330
|
async function promptAgents(detectedAgents) {
|
|
316
331
|
const options = [
|
|
317
332
|
{ value: "antigravity", label: "Antigravity", hint: ".agents/skills (Google Agentic IDE)" },
|
|
@@ -320,7 +335,7 @@ async function promptAgents(detectedAgents) {
|
|
|
320
335
|
{ value: "gemini", label: "Gemini Code Assist", hint: ".gemini/skills (Google Gemini)" }
|
|
321
336
|
];
|
|
322
337
|
const initialValues = detectedAgents.length > 0 ? detectedAgents : ["antigravity"];
|
|
323
|
-
const selected = await
|
|
338
|
+
const selected = await prompts2.multiselect({
|
|
324
339
|
message: "Which AI agents should be configured?",
|
|
325
340
|
options,
|
|
326
341
|
initialValues,
|
|
@@ -330,16 +345,16 @@ async function promptAgents(detectedAgents) {
|
|
|
330
345
|
}
|
|
331
346
|
|
|
332
347
|
// src/domains/ui/prompt-language.ts
|
|
333
|
-
import * as
|
|
348
|
+
import * as prompts3 from "@clack/prompts";
|
|
334
349
|
async function promptLanguage(defaultLanguage = "es") {
|
|
335
350
|
const options = [
|
|
336
|
-
{ value: "es", label: "Espa\xF1ol (Spanish)", hint: "
|
|
351
|
+
{ value: "es", label: "Espa\xF1ol (Spanish)", hint: "Recommended for Spanish-speaking projects" },
|
|
337
352
|
{ value: "en", label: "English", hint: "Standard international English" },
|
|
338
|
-
{ value: "pt", label: "Portugu\xEAs (Portuguese)", hint: "
|
|
339
|
-
{ value: "it", label: "Italiano (Italian)", hint: "
|
|
340
|
-
{ value: "fr", label: "Fran\xE7ais (French)", hint: "
|
|
353
|
+
{ value: "pt", label: "Portugu\xEAs (Portuguese)", hint: "Portuguese for Brazil and Portugal" },
|
|
354
|
+
{ value: "it", label: "Italiano (Italian)", hint: "Italian language communication" },
|
|
355
|
+
{ value: "fr", label: "Fran\xE7ais (French)", hint: "French language communication" }
|
|
341
356
|
];
|
|
342
|
-
const selected = await
|
|
357
|
+
const selected = await prompts3.select({
|
|
343
358
|
message: "Which language should AI agents use for user-facing copy and communication?",
|
|
344
359
|
options,
|
|
345
360
|
initialValue: defaultLanguage
|
|
@@ -348,9 +363,9 @@ async function promptLanguage(defaultLanguage = "es") {
|
|
|
348
363
|
}
|
|
349
364
|
|
|
350
365
|
// src/domains/ui/prompt-confirm-overwrite.ts
|
|
351
|
-
import * as
|
|
366
|
+
import * as prompts4 from "@clack/prompts";
|
|
352
367
|
async function promptConfirmOverwrite() {
|
|
353
|
-
return await
|
|
368
|
+
return await prompts4.confirm({
|
|
354
369
|
message: "Some skills already exist in this project. Would you like to overwrite them with the latest version?",
|
|
355
370
|
initialValue: true
|
|
356
371
|
});
|
|
@@ -360,10 +375,10 @@ async function promptConfirmOverwrite() {
|
|
|
360
375
|
import { lstatSync } from "fs";
|
|
361
376
|
import { join as join3 } from "path";
|
|
362
377
|
var AGENT_DESTINATION_MAP = {
|
|
363
|
-
antigravity: (
|
|
364
|
-
claude: (
|
|
365
|
-
cursor: (
|
|
366
|
-
gemini: (
|
|
378
|
+
antigravity: (directoryName) => join3(".agents", "skills", directoryName),
|
|
379
|
+
claude: (directoryName) => join3(".claude", "skills", directoryName),
|
|
380
|
+
cursor: (directoryName) => join3(".cursor", "rules", `${directoryName}.mdc`),
|
|
381
|
+
gemini: (directoryName) => join3(".gemini", "skills", directoryName)
|
|
367
382
|
};
|
|
368
383
|
function verifySkillExists(targetPath, skillDirectoryName, agents) {
|
|
369
384
|
for (const agent of agents) {
|
|
@@ -379,10 +394,10 @@ function verifySkillExists(targetPath, skillDirectoryName, agents) {
|
|
|
379
394
|
}
|
|
380
395
|
|
|
381
396
|
// src/domains/installer/install-orchestrator.ts
|
|
382
|
-
import { join as
|
|
397
|
+
import { join as join12 } from "path";
|
|
383
398
|
|
|
384
399
|
// src/domains/adapters/skill-reference-builder.ts
|
|
385
|
-
import { existsSync as
|
|
400
|
+
import { existsSync as existsSync5, lstatSync as lstatSync3, mkdirSync, readFileSync as readFileSync3, rmSync as rmSync2, unlinkSync } from "fs";
|
|
386
401
|
import { join as join5 } from "path";
|
|
387
402
|
|
|
388
403
|
// src/domains/project-store/project-paths.ts
|
|
@@ -403,12 +418,195 @@ function getProjectConfigPath(targetPath) {
|
|
|
403
418
|
return join4(getProjectStoreDir(targetPath), "config.json");
|
|
404
419
|
}
|
|
405
420
|
|
|
421
|
+
// src/domains/fs/safe-file-writer.ts
|
|
422
|
+
import { chmodSync, existsSync as existsSync4, renameSync, rmSync, writeFileSync } from "fs";
|
|
423
|
+
|
|
424
|
+
// src/domains/fs/inspect-file-access.ts
|
|
425
|
+
import { execFileSync } from "child_process";
|
|
426
|
+
import { accessSync, constants, existsSync as existsSync3, lstatSync as lstatSync2 } from "fs";
|
|
427
|
+
import { dirname } from "path";
|
|
428
|
+
var UF_IMMUTABLE = 2;
|
|
429
|
+
var SF_IMMUTABLE = 131072;
|
|
430
|
+
function resolveNearestExistingDir(startDir) {
|
|
431
|
+
let current = startDir;
|
|
432
|
+
while (!existsSync3(current)) {
|
|
433
|
+
const parent = dirname(current);
|
|
434
|
+
if (parent === current) {
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
current = parent;
|
|
438
|
+
}
|
|
439
|
+
return current;
|
|
440
|
+
}
|
|
441
|
+
function detectMacOsImmutableFlag(filePath, lstat) {
|
|
442
|
+
const rawFlags = lstat.flags;
|
|
443
|
+
if (typeof rawFlags === "number") {
|
|
444
|
+
return Boolean(rawFlags & (UF_IMMUTABLE | SF_IMMUTABLE));
|
|
445
|
+
}
|
|
446
|
+
if (process.platform !== "darwin") {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
const out = execFileSync("stat", ["-f", "%f", filePath], {
|
|
451
|
+
encoding: "utf-8",
|
|
452
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
453
|
+
}).trim();
|
|
454
|
+
const parsedFlags = Number.parseInt(out, 10);
|
|
455
|
+
if (Number.isNaN(parsedFlags)) {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
return Boolean(parsedFlags & (UF_IMMUTABLE | SF_IMMUTABLE));
|
|
459
|
+
} catch {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function inspectFileAccess(filePath) {
|
|
464
|
+
const currentUid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
465
|
+
const parentDir = dirname(filePath);
|
|
466
|
+
const checkDir = resolveNearestExistingDir(parentDir);
|
|
467
|
+
let dirWritable = false;
|
|
468
|
+
try {
|
|
469
|
+
accessSync(checkDir, constants.W_OK);
|
|
470
|
+
dirWritable = true;
|
|
471
|
+
} catch {
|
|
472
|
+
dirWritable = false;
|
|
473
|
+
}
|
|
474
|
+
let lstat = null;
|
|
475
|
+
try {
|
|
476
|
+
lstat = lstatSync2(filePath);
|
|
477
|
+
} catch {
|
|
478
|
+
lstat = null;
|
|
479
|
+
}
|
|
480
|
+
if (!lstat) {
|
|
481
|
+
return {
|
|
482
|
+
filePath,
|
|
483
|
+
exists: false,
|
|
484
|
+
isSymlink: false,
|
|
485
|
+
ownedByCurrentUser: true,
|
|
486
|
+
ownerUid: null,
|
|
487
|
+
currentUid,
|
|
488
|
+
mode: null,
|
|
489
|
+
immutableFlag: false,
|
|
490
|
+
dirWritable,
|
|
491
|
+
parentDir
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const isSymlink = lstat.isSymbolicLink();
|
|
495
|
+
const ownerUid = lstat.uid;
|
|
496
|
+
const ownedByCurrentUser = currentUid === null || ownerUid === currentUid;
|
|
497
|
+
const mode = lstat.mode & 511;
|
|
498
|
+
const immutableFlag = detectMacOsImmutableFlag(filePath, lstat);
|
|
499
|
+
return {
|
|
500
|
+
filePath,
|
|
501
|
+
exists: true,
|
|
502
|
+
isSymlink,
|
|
503
|
+
ownedByCurrentUser,
|
|
504
|
+
ownerUid,
|
|
505
|
+
currentUid,
|
|
506
|
+
mode,
|
|
507
|
+
immutableFlag,
|
|
508
|
+
dirWritable,
|
|
509
|
+
parentDir
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function formatOwnerName(uid) {
|
|
513
|
+
if (uid === 0) {
|
|
514
|
+
return "root";
|
|
515
|
+
}
|
|
516
|
+
if (uid === null) {
|
|
517
|
+
return "another user";
|
|
518
|
+
}
|
|
519
|
+
return `uid ${uid}`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/errors/file-permission-error.ts
|
|
523
|
+
function buildDiagnosisReason(diagnosis) {
|
|
524
|
+
if (diagnosis.immutableFlag) {
|
|
525
|
+
return "immutable flag (uchg) is set";
|
|
526
|
+
}
|
|
527
|
+
if (diagnosis.exists && !diagnosis.ownedByCurrentUser) {
|
|
528
|
+
return `owned by ${formatOwnerName(diagnosis.ownerUid)}`;
|
|
529
|
+
}
|
|
530
|
+
if (!diagnosis.dirWritable) {
|
|
531
|
+
return `parent directory '${diagnosis.parentDir}' is not writable`;
|
|
532
|
+
}
|
|
533
|
+
return "permission denied";
|
|
534
|
+
}
|
|
535
|
+
function buildSuggestedFix(diagnosis) {
|
|
536
|
+
if (diagnosis.immutableFlag) {
|
|
537
|
+
return `chflags nouchg "${diagnosis.filePath}"`;
|
|
538
|
+
}
|
|
539
|
+
if (diagnosis.exists && !diagnosis.ownedByCurrentUser) {
|
|
540
|
+
return `sudo chown "$(id -un)" "${diagnosis.filePath}"`;
|
|
541
|
+
}
|
|
542
|
+
if (!diagnosis.dirWritable) {
|
|
543
|
+
return `chmod u+w "${diagnosis.parentDir}"`;
|
|
544
|
+
}
|
|
545
|
+
return `chmod u+w "${diagnosis.filePath}"`;
|
|
546
|
+
}
|
|
547
|
+
var FilePermissionError = class extends Error {
|
|
548
|
+
code = "EACCES";
|
|
549
|
+
filePath;
|
|
550
|
+
diagnosis;
|
|
551
|
+
suggestedFixCommand;
|
|
552
|
+
constructor(filePath, diagnosis) {
|
|
553
|
+
const resolved = diagnosis ?? inspectFileAccess(filePath);
|
|
554
|
+
const reason = buildDiagnosisReason(resolved);
|
|
555
|
+
super(`Cannot write to ${filePath} (${reason})`);
|
|
556
|
+
this.name = "FilePermissionError";
|
|
557
|
+
this.filePath = filePath;
|
|
558
|
+
this.diagnosis = resolved;
|
|
559
|
+
this.suggestedFixCommand = buildSuggestedFix(resolved);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// src/domains/fs/safe-file-writer.ts
|
|
564
|
+
function safeWriteFileSync(filePath, content, options) {
|
|
565
|
+
const chmodFunction = options?.chmodImpl ?? chmodSync;
|
|
566
|
+
try {
|
|
567
|
+
writeFileSync(filePath, content, "utf-8");
|
|
568
|
+
return { strategy: "direct", previousOwnerUid: null };
|
|
569
|
+
} catch (error) {
|
|
570
|
+
const nodeError = error;
|
|
571
|
+
if (nodeError.code !== "EACCES" && nodeError.code !== "EPERM") {
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const diagnosis = inspectFileAccess(filePath);
|
|
576
|
+
if (diagnosis.exists && diagnosis.ownedByCurrentUser && !diagnosis.immutableFlag) {
|
|
577
|
+
try {
|
|
578
|
+
const targetMode = (diagnosis.mode ?? 420) | 128;
|
|
579
|
+
chmodFunction(filePath, targetMode);
|
|
580
|
+
writeFileSync(filePath, content, "utf-8");
|
|
581
|
+
return { strategy: "chmod", previousOwnerUid: diagnosis.ownerUid };
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (diagnosis.exists && diagnosis.dirWritable && !diagnosis.immutableFlag) {
|
|
586
|
+
const tempPath = `${filePath}.groove-tmp-${process.pid}-${Date.now()}`;
|
|
587
|
+
try {
|
|
588
|
+
const preservedMode = (diagnosis.mode ?? 420) | 128;
|
|
589
|
+
writeFileSync(tempPath, content, { encoding: "utf-8", mode: preservedMode });
|
|
590
|
+
renameSync(tempPath, filePath);
|
|
591
|
+
return { strategy: "replaced", previousOwnerUid: diagnosis.ownerUid };
|
|
592
|
+
} catch {
|
|
593
|
+
if (existsSync4(tempPath)) {
|
|
594
|
+
try {
|
|
595
|
+
rmSync(tempPath, { force: true });
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
throw new FilePermissionError(filePath, diagnosis);
|
|
602
|
+
}
|
|
603
|
+
|
|
406
604
|
// src/domains/adapters/skill-reference-builder.ts
|
|
407
605
|
function removeExistingPathSafely(path) {
|
|
408
|
-
const fileExists =
|
|
606
|
+
const fileExists = existsSync5(path);
|
|
409
607
|
if (!fileExists) {
|
|
410
608
|
try {
|
|
411
|
-
const lstat =
|
|
609
|
+
const lstat = lstatSync3(path);
|
|
412
610
|
if (lstat.isSymbolicLink()) {
|
|
413
611
|
unlinkSync(path);
|
|
414
612
|
}
|
|
@@ -417,13 +615,13 @@ function removeExistingPathSafely(path) {
|
|
|
417
615
|
return;
|
|
418
616
|
}
|
|
419
617
|
try {
|
|
420
|
-
const lstat =
|
|
618
|
+
const lstat = lstatSync3(path);
|
|
421
619
|
if (lstat.isSymbolicLink()) {
|
|
422
620
|
unlinkSync(path);
|
|
423
621
|
return;
|
|
424
622
|
}
|
|
425
623
|
if (lstat.isDirectory()) {
|
|
426
|
-
|
|
624
|
+
rmSync2(path, { recursive: true, force: true });
|
|
427
625
|
return;
|
|
428
626
|
}
|
|
429
627
|
unlinkSync(path);
|
|
@@ -431,7 +629,7 @@ function removeExistingPathSafely(path) {
|
|
|
431
629
|
}
|
|
432
630
|
}
|
|
433
631
|
function extractSkillDescription(canonicalSkillMdPath, fallback) {
|
|
434
|
-
if (!
|
|
632
|
+
if (!existsSync5(canonicalSkillMdPath)) {
|
|
435
633
|
return fallback;
|
|
436
634
|
}
|
|
437
635
|
try {
|
|
@@ -445,8 +643,14 @@ function extractSkillDescription(canonicalSkillMdPath, fallback) {
|
|
|
445
643
|
}
|
|
446
644
|
return fallback;
|
|
447
645
|
}
|
|
448
|
-
function writeAgentSkillReference(targetPath, agentRelativeSkillsDir, skill) {
|
|
646
|
+
function writeAgentSkillReference(targetPath, agentRelativeSkillsDir, skill, transaction) {
|
|
449
647
|
const agentSkillFolder = join5(targetPath, agentRelativeSkillsDir, skill.directoryName);
|
|
648
|
+
const targetFile = join5(agentSkillFolder, "SKILL.md");
|
|
649
|
+
if (transaction) {
|
|
650
|
+
transaction.recordCreatedDir(join5(targetPath, agentRelativeSkillsDir));
|
|
651
|
+
transaction.recordCreatedDir(agentSkillFolder);
|
|
652
|
+
transaction.recordBeforeWrite(targetFile);
|
|
653
|
+
}
|
|
450
654
|
removeExistingPathSafely(agentSkillFolder);
|
|
451
655
|
mkdirSync(agentSkillFolder, { recursive: true });
|
|
452
656
|
const canonicalSkillMd = join5(getProjectSkillsDir(targetPath), skill.directoryName, "SKILL.md");
|
|
@@ -465,16 +669,19 @@ Canonical skill specification stored in \`.grooveskills/\`.
|
|
|
465
669
|
|
|
466
670
|
> **Instruction**: Read \`.grooveskills/skills/${skill.directoryName}/SKILL.md\` before executing tasks matching this skill.
|
|
467
671
|
`;
|
|
468
|
-
|
|
469
|
-
writeFileSync(targetFile, referenceContent, "utf-8");
|
|
672
|
+
safeWriteFileSync(targetFile, referenceContent);
|
|
470
673
|
return targetFile;
|
|
471
674
|
}
|
|
472
|
-
function writeCursorRuleReference(targetPath, cursorRelativeRulesDir, skill) {
|
|
675
|
+
function writeCursorRuleReference(targetPath, cursorRelativeRulesDir, skill, transaction) {
|
|
473
676
|
const rulesDir = join5(targetPath, cursorRelativeRulesDir);
|
|
474
|
-
|
|
677
|
+
const targetFile = join5(rulesDir, `${skill.id}.mdc`);
|
|
678
|
+
if (transaction) {
|
|
679
|
+
transaction.recordCreatedDir(rulesDir);
|
|
680
|
+
transaction.recordBeforeWrite(targetFile);
|
|
681
|
+
}
|
|
682
|
+
if (!existsSync5(rulesDir)) {
|
|
475
683
|
mkdirSync(rulesDir, { recursive: true });
|
|
476
684
|
}
|
|
477
|
-
const targetFile = join5(rulesDir, `${skill.id}.mdc`);
|
|
478
685
|
removeExistingPathSafely(targetFile);
|
|
479
686
|
const canonicalSkillMd = join5(getProjectSkillsDir(targetPath), skill.directoryName, "SKILL.md");
|
|
480
687
|
const description = extractSkillDescription(canonicalSkillMd, skill.description);
|
|
@@ -493,7 +700,7 @@ Canonical rule specification stored in \`.grooveskills/\`.
|
|
|
493
700
|
|
|
494
701
|
> **Instruction**: Read \`.grooveskills/skills/${skill.directoryName}/SKILL.md\` before executing tasks matching this rule.
|
|
495
702
|
`;
|
|
496
|
-
|
|
703
|
+
safeWriteFileSync(targetFile, referenceContent);
|
|
497
704
|
return targetFile;
|
|
498
705
|
}
|
|
499
706
|
|
|
@@ -502,8 +709,8 @@ var AntigravityAdapter = class {
|
|
|
502
709
|
id = "antigravity";
|
|
503
710
|
name = "Antigravity";
|
|
504
711
|
targetRelativeDir = ".agents/skills";
|
|
505
|
-
async installSkill(targetPath, skill, _skillSourceDir) {
|
|
506
|
-
const file = writeAgentSkillReference(targetPath, this.targetRelativeDir, skill);
|
|
712
|
+
async installSkill(targetPath, skill, _skillSourceDir, transaction) {
|
|
713
|
+
const file = writeAgentSkillReference(targetPath, this.targetRelativeDir, skill, transaction);
|
|
507
714
|
return [file];
|
|
508
715
|
}
|
|
509
716
|
};
|
|
@@ -513,8 +720,8 @@ var ClaudeAdapter = class {
|
|
|
513
720
|
id = "claude";
|
|
514
721
|
name = "Claude Code";
|
|
515
722
|
targetRelativeDir = ".claude/skills";
|
|
516
|
-
async installSkill(targetPath, skill, _skillSourceDir) {
|
|
517
|
-
const file = writeAgentSkillReference(targetPath, this.targetRelativeDir, skill);
|
|
723
|
+
async installSkill(targetPath, skill, _skillSourceDir, transaction) {
|
|
724
|
+
const file = writeAgentSkillReference(targetPath, this.targetRelativeDir, skill, transaction);
|
|
518
725
|
return [file];
|
|
519
726
|
}
|
|
520
727
|
};
|
|
@@ -524,8 +731,8 @@ var CursorAdapter = class {
|
|
|
524
731
|
id = "cursor";
|
|
525
732
|
name = "Cursor";
|
|
526
733
|
targetRelativeDir = ".cursor/rules";
|
|
527
|
-
async installSkill(targetPath, skill, _skillSourceDir) {
|
|
528
|
-
const file = writeCursorRuleReference(targetPath, this.targetRelativeDir, skill);
|
|
734
|
+
async installSkill(targetPath, skill, _skillSourceDir, transaction) {
|
|
735
|
+
const file = writeCursorRuleReference(targetPath, this.targetRelativeDir, skill, transaction);
|
|
529
736
|
return [file];
|
|
530
737
|
}
|
|
531
738
|
};
|
|
@@ -535,38 +742,38 @@ var GeminiAdapter = class {
|
|
|
535
742
|
id = "gemini";
|
|
536
743
|
name = "Gemini Code Assist";
|
|
537
744
|
targetRelativeDir = ".gemini/skills";
|
|
538
|
-
async installSkill(targetPath, skill, _skillSourceDir) {
|
|
539
|
-
const file = writeAgentSkillReference(targetPath, this.targetRelativeDir, skill);
|
|
745
|
+
async installSkill(targetPath, skill, _skillSourceDir, transaction) {
|
|
746
|
+
const file = writeAgentSkillReference(targetPath, this.targetRelativeDir, skill, transaction);
|
|
540
747
|
return [file];
|
|
541
748
|
}
|
|
542
749
|
};
|
|
543
750
|
|
|
544
751
|
// src/domains/project-store/sync-project-store.ts
|
|
545
|
-
import { cpSync, existsSync as
|
|
752
|
+
import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync2, rmSync as rmSync3 } from "fs";
|
|
546
753
|
import { join as join7 } from "path";
|
|
547
754
|
|
|
548
755
|
// src/domains/installer/resolve-skills-source.ts
|
|
549
756
|
import { fileURLToPath } from "url";
|
|
550
|
-
import { dirname, resolve, join as join6 } from "path";
|
|
551
|
-
import { existsSync as
|
|
757
|
+
import { dirname as dirname2, resolve, join as join6 } from "path";
|
|
758
|
+
import { existsSync as existsSync6 } from "fs";
|
|
552
759
|
function resolveSkillsSourceRoot() {
|
|
553
760
|
const currentFilePath = fileURLToPath(import.meta.url);
|
|
554
|
-
const currentDir =
|
|
761
|
+
const currentDir = dirname2(currentFilePath);
|
|
555
762
|
const candidateFromDist = resolve(currentDir, "../skills");
|
|
556
|
-
if (
|
|
763
|
+
if (existsSync6(candidateFromDist)) {
|
|
557
764
|
return candidateFromDist;
|
|
558
765
|
}
|
|
559
766
|
const candidateFromSrc = resolve(currentDir, "../../../skills");
|
|
560
|
-
if (
|
|
767
|
+
if (existsSync6(candidateFromSrc)) {
|
|
561
768
|
return candidateFromSrc;
|
|
562
769
|
}
|
|
563
770
|
let searchDir = currentDir;
|
|
564
|
-
for (let
|
|
771
|
+
for (let depth = 0; depth < 5; depth++) {
|
|
565
772
|
const candidate = join6(searchDir, "skills");
|
|
566
|
-
if (
|
|
773
|
+
if (existsSync6(candidate)) {
|
|
567
774
|
return candidate;
|
|
568
775
|
}
|
|
569
|
-
const parent =
|
|
776
|
+
const parent = dirname2(searchDir);
|
|
570
777
|
if (parent === searchDir) {
|
|
571
778
|
break;
|
|
572
779
|
}
|
|
@@ -578,16 +785,19 @@ function resolveSkillsSourceRoot() {
|
|
|
578
785
|
}
|
|
579
786
|
|
|
580
787
|
// src/domains/project-store/sync-project-store.ts
|
|
581
|
-
function syncProjectStore(targetPath, selectedSkillIds, _selectedAgents) {
|
|
788
|
+
function syncProjectStore(targetPath, selectedSkillIds, _selectedAgents, transaction) {
|
|
582
789
|
const bundledSkillsDir = resolveSkillsSourceRoot();
|
|
583
790
|
const projectSkillsDir = getProjectSkillsDir(targetPath);
|
|
584
|
-
if (
|
|
791
|
+
if (transaction) {
|
|
792
|
+
transaction.recordCreatedDir(projectSkillsDir);
|
|
793
|
+
}
|
|
794
|
+
if (!existsSync7(projectSkillsDir)) {
|
|
585
795
|
mkdirSync2(projectSkillsDir, { recursive: true });
|
|
586
796
|
}
|
|
587
797
|
const legacyCursorRules = join7(targetPath, ".grooveskills", "cursor-rules");
|
|
588
|
-
if (
|
|
798
|
+
if (existsSync7(legacyCursorRules)) {
|
|
589
799
|
try {
|
|
590
|
-
|
|
800
|
+
rmSync3(legacyCursorRules, { recursive: true, force: true });
|
|
591
801
|
} catch {
|
|
592
802
|
}
|
|
593
803
|
}
|
|
@@ -598,14 +808,17 @@ function syncProjectStore(targetPath, selectedSkillIds, _selectedAgents) {
|
|
|
598
808
|
}
|
|
599
809
|
const sourceSkillDir = join7(bundledSkillsDir, skill.directoryName);
|
|
600
810
|
const destSkillDir = join7(projectSkillsDir, skill.directoryName);
|
|
601
|
-
if (
|
|
811
|
+
if (transaction) {
|
|
812
|
+
transaction.recordCreatedDir(destSkillDir);
|
|
813
|
+
}
|
|
814
|
+
if (existsSync7(sourceSkillDir)) {
|
|
602
815
|
cpSync(sourceSkillDir, destSkillDir, { recursive: true });
|
|
603
816
|
}
|
|
604
817
|
}
|
|
605
818
|
}
|
|
606
819
|
|
|
607
820
|
// src/domains/harness/install-harness.ts
|
|
608
|
-
import { existsSync as
|
|
821
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "fs";
|
|
609
822
|
import { join as join10 } from "path";
|
|
610
823
|
|
|
611
824
|
// src/domains/harness/harness-languages.ts
|
|
@@ -654,14 +867,14 @@ var SUPPORTED_LANGUAGES = {
|
|
|
654
867
|
|
|
655
868
|
// src/domains/harness/harness-templates.ts
|
|
656
869
|
function buildContextTemplate(detectedDependencies, language = "es", profile) {
|
|
657
|
-
const
|
|
658
|
-
const
|
|
870
|
+
const dependenciesFormatted = detectedDependencies.length > 0 ? detectedDependencies.map((dependencyName) => `- \`${dependencyName}\``).join("\n") : "- (No external dependencies detected)";
|
|
871
|
+
const languageConfig = SUPPORTED_LANGUAGES[language];
|
|
659
872
|
const projectName = profile?.projectName ?? "Project";
|
|
660
|
-
const
|
|
873
|
+
const packageManager = profile?.packageManager ?? "npm";
|
|
661
874
|
const commandLines = [];
|
|
662
875
|
if (profile?.commands) {
|
|
663
|
-
for (const [
|
|
664
|
-
commandLines.push(`- **${
|
|
876
|
+
for (const [commandKey, commandValue] of Object.entries(profile.commands)) {
|
|
877
|
+
commandLines.push(`- **${commandKey}**: \`${commandValue}\``);
|
|
665
878
|
}
|
|
666
879
|
}
|
|
667
880
|
const commandsSection = commandLines.length > 0 ? commandLines.join("\n") : `- Build: Refer to \`package.json\` scripts.
|
|
@@ -669,62 +882,62 @@ function buildContextTemplate(detectedDependencies, language = "es", profile) {
|
|
|
669
882
|
- Tests: Refer to test runner configuration.`;
|
|
670
883
|
const aliasLines = [];
|
|
671
884
|
if (profile?.pathAliases) {
|
|
672
|
-
for (const [
|
|
673
|
-
aliasLines.push(`- \`${
|
|
885
|
+
for (const [aliasPattern, aliasTarget] of Object.entries(profile.pathAliases)) {
|
|
886
|
+
aliasLines.push(`- \`${aliasPattern}\` -> \`${aliasTarget}\``);
|
|
674
887
|
}
|
|
675
888
|
}
|
|
676
889
|
const aliasesSection = aliasLines.length > 0 ? `
|
|
677
890
|
### Path Aliases
|
|
678
891
|
${aliasLines.join("\n")}
|
|
679
892
|
` : "";
|
|
680
|
-
const
|
|
893
|
+
const directoryLines = [];
|
|
681
894
|
if (profile?.sourceDirectories && profile.sourceDirectories.length > 0) {
|
|
682
|
-
for (const
|
|
683
|
-
|
|
895
|
+
for (const directoryName of profile.sourceDirectories) {
|
|
896
|
+
directoryLines.push(`- \`${directoryName}/\``);
|
|
684
897
|
}
|
|
685
898
|
}
|
|
686
|
-
const
|
|
899
|
+
const directoriesSection = directoryLines.length > 0 ? `
|
|
687
900
|
### Source Modules
|
|
688
|
-
${
|
|
901
|
+
${directoryLines.join("\n")}
|
|
689
902
|
` : "";
|
|
690
903
|
return `# Project Context & Architecture: ${projectName}
|
|
691
904
|
|
|
692
905
|
This file provides AI agents with instant architectural orientation for this project.
|
|
693
906
|
|
|
694
907
|
## 1. Stack & Dependencies
|
|
695
|
-
- **Package Manager**: \`${
|
|
696
|
-
${
|
|
908
|
+
- **Package Manager**: \`${packageManager}\`
|
|
909
|
+
${dependenciesFormatted}
|
|
697
910
|
|
|
698
911
|
## 2. Directory Structure Conventions
|
|
699
912
|
- Follow single-responsibility per file (<200 lines).
|
|
700
913
|
- Maintain direct imports with path aliases; avoid barrel files.
|
|
701
914
|
- Keep domain boundaries explicit and clean.
|
|
702
|
-
${aliasesSection}${
|
|
915
|
+
${aliasesSection}${directoriesSection}
|
|
703
916
|
## 3. Communication & User Copy Language
|
|
704
|
-
- **Configured Language**: ${
|
|
705
|
-
- **Policy**: ${
|
|
917
|
+
- **Configured Language**: ${languageConfig.nativeName} (\`${language}\`).
|
|
918
|
+
- **Policy**: ${languageConfig.instruction}
|
|
706
919
|
|
|
707
920
|
## 4. Essential Commands
|
|
708
921
|
${commandsSection}
|
|
709
922
|
`;
|
|
710
923
|
}
|
|
711
924
|
function buildDecisionsTemplate(language = "es") {
|
|
712
|
-
const
|
|
713
|
-
const
|
|
925
|
+
const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
926
|
+
const languageConfig = SUPPORTED_LANGUAGES[language];
|
|
714
927
|
return `# Architecture Decision Records (ADR)
|
|
715
928
|
|
|
716
929
|
An append-only log of significant technical and architectural choices.
|
|
717
930
|
|
|
718
931
|
| Date | Title | Decision | Reason & Consequences |
|
|
719
932
|
| :--- | :--- | :--- | :--- |
|
|
720
|
-
| ${
|
|
933
|
+
| ${currentDate} | Adopt GrooveSkills Harness | Initialized \`.grooveskills/\` control plane with ODD | Clean, encapsulated agent memory, 3-lane ODD flow, and ${languageConfig.nativeName} copy standard |
|
|
721
934
|
`;
|
|
722
935
|
}
|
|
723
936
|
function buildOnboardingTaskTemplate(projectName = "Project") {
|
|
724
|
-
const
|
|
937
|
+
const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
725
938
|
return `---
|
|
726
939
|
status: pending
|
|
727
|
-
createdAt: ${
|
|
940
|
+
createdAt: ${currentDate}
|
|
728
941
|
---
|
|
729
942
|
|
|
730
943
|
# 00 \u2014 Initial Project Onboarding & Architecture Exploration: ${projectName}
|
|
@@ -780,7 +993,7 @@ By [Output / Technical Change], [Outcome / Verifiable Behavior] will occur, prov
|
|
|
780
993
|
`;
|
|
781
994
|
}
|
|
782
995
|
function buildAgentHarnessBlock(language = "es") {
|
|
783
|
-
const
|
|
996
|
+
const languageConfig = SUPPORTED_LANGUAGES[language];
|
|
784
997
|
return `<!-- grooveskills:harness:start -->
|
|
785
998
|
# GrooveSkills Harness \u2014 Agent Rules & Memory
|
|
786
999
|
|
|
@@ -793,9 +1006,9 @@ This project uses the **GrooveSkills Harness** located in \`.grooveskills/\`.
|
|
|
793
1006
|
- **Lane 2 (Task)**: Direct implementation. Declare Outcome Hypothesis and checklist in \`.grooveskills/memory/tasks/<slug>.md\`. Only check off tasks with observed evidence under \`Evidence\`.
|
|
794
1007
|
- **Lane 3 (Spec)**: Ambiguous or high-impact architecture. Clarify and validate the spec in \`.grooveskills/memory/tasks/<slug>.md\` before writing code.
|
|
795
1008
|
|
|
796
|
-
## 2. User Communication Language (${
|
|
797
|
-
- **Language**: \`${language}\` (${
|
|
798
|
-
- **Requirement**: ${
|
|
1009
|
+
## 2. User Communication Language (${languageConfig.nativeName})
|
|
1010
|
+
- **Language**: \`${language}\` (${languageConfig.nativeName}).
|
|
1011
|
+
- **Requirement**: ${languageConfig.instruction}
|
|
799
1012
|
|
|
800
1013
|
## 3. Memory Locations
|
|
801
1014
|
- Architecture context: \`.grooveskills/memory/context.md\`
|
|
@@ -805,11 +1018,11 @@ This project uses the **GrooveSkills Harness** located in \`.grooveskills/\`.
|
|
|
805
1018
|
}
|
|
806
1019
|
|
|
807
1020
|
// src/domains/harness/scan-project-onboarding.ts
|
|
808
|
-
import { existsSync as
|
|
1021
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
809
1022
|
import { basename, join as join8 } from "path";
|
|
810
1023
|
function detectProjectName(targetPath) {
|
|
811
1024
|
const packageJsonPath = join8(targetPath, "package.json");
|
|
812
|
-
if (
|
|
1025
|
+
if (existsSync8(packageJsonPath)) {
|
|
813
1026
|
try {
|
|
814
1027
|
const raw = readFileSync4(packageJsonPath, "utf-8");
|
|
815
1028
|
const parsed = JSON.parse(raw);
|
|
@@ -820,7 +1033,7 @@ function detectProjectName(targetPath) {
|
|
|
820
1033
|
}
|
|
821
1034
|
}
|
|
822
1035
|
const cargoPath = join8(targetPath, "Cargo.toml");
|
|
823
|
-
if (
|
|
1036
|
+
if (existsSync8(cargoPath)) {
|
|
824
1037
|
try {
|
|
825
1038
|
const cargoRaw = readFileSync4(cargoPath, "utf-8");
|
|
826
1039
|
const nameMatch = cargoRaw.match(/name\s*=\s*"([^"]+)"/);
|
|
@@ -831,12 +1044,12 @@ function detectProjectName(targetPath) {
|
|
|
831
1044
|
}
|
|
832
1045
|
}
|
|
833
1046
|
const pyprojectPath = join8(targetPath, "pyproject.toml");
|
|
834
|
-
if (
|
|
1047
|
+
if (existsSync8(pyprojectPath)) {
|
|
835
1048
|
try {
|
|
836
|
-
const
|
|
837
|
-
const
|
|
838
|
-
if (
|
|
839
|
-
return
|
|
1049
|
+
const pyprojectRaw = readFileSync4(pyprojectPath, "utf-8");
|
|
1050
|
+
const nameMatch = pyprojectRaw.match(/name\s*=\s*"([^"]+)"/);
|
|
1051
|
+
if (nameMatch?.[1]) {
|
|
1052
|
+
return nameMatch[1];
|
|
840
1053
|
}
|
|
841
1054
|
} catch {
|
|
842
1055
|
}
|
|
@@ -844,32 +1057,32 @@ function detectProjectName(targetPath) {
|
|
|
844
1057
|
return basename(targetPath) || "project";
|
|
845
1058
|
}
|
|
846
1059
|
function detectPackageManager(targetPath) {
|
|
847
|
-
if (
|
|
1060
|
+
if (existsSync8(join8(targetPath, "pnpm-lock.yaml"))) {
|
|
848
1061
|
return "pnpm";
|
|
849
1062
|
}
|
|
850
|
-
if (
|
|
1063
|
+
if (existsSync8(join8(targetPath, "bun.lockb")) || existsSync8(join8(targetPath, "bun.lock"))) {
|
|
851
1064
|
return "bun";
|
|
852
1065
|
}
|
|
853
|
-
if (
|
|
1066
|
+
if (existsSync8(join8(targetPath, "yarn.lock"))) {
|
|
854
1067
|
return "yarn";
|
|
855
1068
|
}
|
|
856
|
-
if (
|
|
1069
|
+
if (existsSync8(join8(targetPath, "package-lock.json"))) {
|
|
857
1070
|
return "npm";
|
|
858
1071
|
}
|
|
859
|
-
if (
|
|
1072
|
+
if (existsSync8(join8(targetPath, "Cargo.lock")) || existsSync8(join8(targetPath, "Cargo.toml"))) {
|
|
860
1073
|
return "cargo";
|
|
861
1074
|
}
|
|
862
|
-
if (
|
|
1075
|
+
if (existsSync8(join8(targetPath, "uv.lock"))) {
|
|
863
1076
|
return "uv";
|
|
864
1077
|
}
|
|
865
|
-
if (
|
|
1078
|
+
if (existsSync8(join8(targetPath, "poetry.lock"))) {
|
|
866
1079
|
return "poetry";
|
|
867
1080
|
}
|
|
868
1081
|
return "npm";
|
|
869
1082
|
}
|
|
870
|
-
function detectCommands(targetPath,
|
|
1083
|
+
function detectCommands(targetPath, packageManager) {
|
|
871
1084
|
const packageJsonPath = join8(targetPath, "package.json");
|
|
872
|
-
if (!
|
|
1085
|
+
if (!existsSync8(packageJsonPath)) {
|
|
873
1086
|
return {};
|
|
874
1087
|
}
|
|
875
1088
|
try {
|
|
@@ -877,7 +1090,7 @@ function detectCommands(targetPath, pkgManager) {
|
|
|
877
1090
|
const parsed = JSON.parse(raw);
|
|
878
1091
|
const scripts = parsed.scripts ?? {};
|
|
879
1092
|
const commands = {};
|
|
880
|
-
const runner =
|
|
1093
|
+
const runner = packageManager === "npm" ? "npm run" : packageManager;
|
|
881
1094
|
if (scripts.build) {
|
|
882
1095
|
commands.build = `${runner} build`;
|
|
883
1096
|
}
|
|
@@ -904,7 +1117,7 @@ function detectCommands(targetPath, pkgManager) {
|
|
|
904
1117
|
}
|
|
905
1118
|
function detectPathAliases(targetPath) {
|
|
906
1119
|
const tsconfigPath = join8(targetPath, "tsconfig.json");
|
|
907
|
-
if (!
|
|
1120
|
+
if (!existsSync8(tsconfigPath)) {
|
|
908
1121
|
return {};
|
|
909
1122
|
}
|
|
910
1123
|
try {
|
|
@@ -912,9 +1125,9 @@ function detectPathAliases(targetPath) {
|
|
|
912
1125
|
const parsed = JSON.parse(raw);
|
|
913
1126
|
const paths = parsed.compilerOptions?.paths ?? {};
|
|
914
1127
|
const aliases = {};
|
|
915
|
-
for (const [
|
|
916
|
-
if (Array.isArray(
|
|
917
|
-
aliases[
|
|
1128
|
+
for (const [aliasKey, aliasTargets] of Object.entries(paths)) {
|
|
1129
|
+
if (Array.isArray(aliasTargets) && typeof aliasTargets[0] === "string") {
|
|
1130
|
+
aliases[aliasKey] = aliasTargets[0];
|
|
918
1131
|
}
|
|
919
1132
|
}
|
|
920
1133
|
return aliases;
|
|
@@ -924,7 +1137,7 @@ function detectPathAliases(targetPath) {
|
|
|
924
1137
|
}
|
|
925
1138
|
function detectSourceDirectories(targetPath) {
|
|
926
1139
|
const srcPath = join8(targetPath, "src");
|
|
927
|
-
const scanTarget =
|
|
1140
|
+
const scanTarget = existsSync8(srcPath) ? srcPath : targetPath;
|
|
928
1141
|
try {
|
|
929
1142
|
const entries = readdirSync2(scanTarget, { withFileTypes: true });
|
|
930
1143
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => !name.startsWith(".") && name !== "node_modules" && name !== "dist");
|
|
@@ -948,53 +1161,76 @@ function scanProjectOnboarding(targetPath) {
|
|
|
948
1161
|
}
|
|
949
1162
|
|
|
950
1163
|
// src/domains/harness/upsert-agent-instructions.ts
|
|
951
|
-
import { existsSync as
|
|
1164
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
952
1165
|
import { join as join9 } from "path";
|
|
953
1166
|
var START_MARKER = "<!-- grooveskills:harness:start -->";
|
|
954
1167
|
var END_MARKER = "<!-- grooveskills:harness:end -->";
|
|
955
|
-
function
|
|
1168
|
+
function writeWithTransaction(filePath, content, transaction) {
|
|
1169
|
+
const outcome = safeWriteFileSync(filePath, content);
|
|
1170
|
+
if (outcome.strategy === "replaced" && transaction) {
|
|
1171
|
+
transaction.recordReplacedFile(filePath, outcome.previousOwnerUid);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function applyHarnessBlock(filePath, language = "es", transaction) {
|
|
1175
|
+
if (transaction) {
|
|
1176
|
+
transaction.recordBeforeWrite(filePath);
|
|
1177
|
+
}
|
|
956
1178
|
const block = buildAgentHarnessBlock(language);
|
|
957
|
-
if (!
|
|
958
|
-
|
|
959
|
-
`,
|
|
1179
|
+
if (!existsSync9(filePath)) {
|
|
1180
|
+
writeWithTransaction(filePath, `${block}
|
|
1181
|
+
`, transaction);
|
|
960
1182
|
return;
|
|
961
1183
|
}
|
|
962
1184
|
const existingContent = readFileSync5(filePath, "utf-8");
|
|
963
1185
|
if (existingContent.includes(START_MARKER) && existingContent.includes(END_MARKER)) {
|
|
964
1186
|
const regex = new RegExp(`${START_MARKER}[\\s\\S]*?${END_MARKER}`, "g");
|
|
965
1187
|
const updated = existingContent.replace(regex, block);
|
|
966
|
-
|
|
1188
|
+
writeWithTransaction(filePath, updated, transaction);
|
|
967
1189
|
return;
|
|
968
1190
|
}
|
|
969
1191
|
const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
|
|
970
|
-
|
|
971
|
-
`,
|
|
1192
|
+
writeWithTransaction(filePath, `${existingContent}${separator}${block}
|
|
1193
|
+
`, transaction);
|
|
972
1194
|
}
|
|
973
|
-
function upsertAgentInstructions(targetPath, selectedAgents, language = "es") {
|
|
1195
|
+
function upsertAgentInstructions(targetPath, selectedAgents, language = "es", transaction) {
|
|
974
1196
|
const updatedFiles = [];
|
|
975
1197
|
const agentsMdPath = join9(targetPath, "AGENTS.md");
|
|
976
|
-
applyHarnessBlock(agentsMdPath, language);
|
|
1198
|
+
applyHarnessBlock(agentsMdPath, language, transaction);
|
|
977
1199
|
updatedFiles.push(agentsMdPath);
|
|
978
1200
|
if (selectedAgents.includes("claude")) {
|
|
979
1201
|
const claudeMdPath = join9(targetPath, "CLAUDE.md");
|
|
980
|
-
applyHarnessBlock(claudeMdPath, language);
|
|
1202
|
+
applyHarnessBlock(claudeMdPath, language, transaction);
|
|
981
1203
|
updatedFiles.push(claudeMdPath);
|
|
982
1204
|
}
|
|
983
1205
|
return updatedFiles;
|
|
984
1206
|
}
|
|
985
1207
|
|
|
986
1208
|
// src/domains/harness/install-harness.ts
|
|
1209
|
+
function writeTrackedFile(filePath, content, transaction) {
|
|
1210
|
+
if (transaction) {
|
|
1211
|
+
transaction.recordBeforeWrite(filePath);
|
|
1212
|
+
}
|
|
1213
|
+
const outcome = safeWriteFileSync(filePath, content);
|
|
1214
|
+
if (outcome.strategy === "replaced" && transaction) {
|
|
1215
|
+
transaction.recordReplacedFile(filePath, outcome.previousOwnerUid);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
987
1218
|
function installHarness(options) {
|
|
988
1219
|
const memoryDir = getProjectMemoryDir(options.targetPath);
|
|
989
1220
|
const tasksDir = getProjectTasksDir(options.targetPath);
|
|
990
1221
|
const language = options.language ?? "es";
|
|
991
1222
|
const forceOnboarding = Boolean(options.forceOnboarding);
|
|
992
|
-
|
|
1223
|
+
const transaction = options.transaction;
|
|
1224
|
+
if (transaction) {
|
|
1225
|
+
transaction.recordCreatedDir(memoryDir);
|
|
1226
|
+
transaction.recordCreatedDir(tasksDir);
|
|
1227
|
+
}
|
|
1228
|
+
if (!existsSync10(tasksDir)) {
|
|
993
1229
|
mkdirSync3(tasksDir, { recursive: true });
|
|
994
1230
|
}
|
|
995
1231
|
const createdFiles = [];
|
|
996
1232
|
const contextPath = join10(memoryDir, "context.md");
|
|
997
|
-
const shouldWriteContext = !
|
|
1233
|
+
const shouldWriteContext = !existsSync10(contextPath) || forceOnboarding;
|
|
998
1234
|
if (shouldWriteContext) {
|
|
999
1235
|
const profile = scanProjectOnboarding(options.targetPath);
|
|
1000
1236
|
const contextContent = buildContextTemplate(
|
|
@@ -1002,30 +1238,31 @@ function installHarness(options) {
|
|
|
1002
1238
|
language,
|
|
1003
1239
|
profile
|
|
1004
1240
|
);
|
|
1005
|
-
|
|
1241
|
+
writeTrackedFile(contextPath, contextContent, transaction);
|
|
1006
1242
|
createdFiles.push(contextPath);
|
|
1007
1243
|
const onboardingTaskPath = join10(tasksDir, "00-onboarding.md");
|
|
1008
|
-
|
|
1244
|
+
writeTrackedFile(
|
|
1009
1245
|
onboardingTaskPath,
|
|
1010
1246
|
buildOnboardingTaskTemplate(profile.projectName),
|
|
1011
|
-
|
|
1247
|
+
transaction
|
|
1012
1248
|
);
|
|
1013
1249
|
createdFiles.push(onboardingTaskPath);
|
|
1014
1250
|
}
|
|
1015
1251
|
const decisionsPath = join10(memoryDir, "decisions.md");
|
|
1016
|
-
if (!
|
|
1017
|
-
|
|
1252
|
+
if (!existsSync10(decisionsPath)) {
|
|
1253
|
+
writeTrackedFile(decisionsPath, buildDecisionsTemplate(language), transaction);
|
|
1018
1254
|
createdFiles.push(decisionsPath);
|
|
1019
1255
|
}
|
|
1020
1256
|
const templateTaskPath = join10(tasksDir, "_template.md");
|
|
1021
|
-
if (!
|
|
1022
|
-
|
|
1257
|
+
if (!existsSync10(templateTaskPath)) {
|
|
1258
|
+
writeTrackedFile(templateTaskPath, buildTaskTemplate(), transaction);
|
|
1023
1259
|
createdFiles.push(templateTaskPath);
|
|
1024
1260
|
}
|
|
1025
1261
|
const instructions = upsertAgentInstructions(
|
|
1026
1262
|
options.targetPath,
|
|
1027
1263
|
options.selectedAgents,
|
|
1028
|
-
language
|
|
1264
|
+
language,
|
|
1265
|
+
transaction
|
|
1029
1266
|
);
|
|
1030
1267
|
createdFiles.push(...instructions);
|
|
1031
1268
|
return {
|
|
@@ -1035,14 +1272,14 @@ function installHarness(options) {
|
|
|
1035
1272
|
}
|
|
1036
1273
|
|
|
1037
1274
|
// src/domains/harness/write-project-config.ts
|
|
1038
|
-
import { existsSync as
|
|
1275
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
|
|
1039
1276
|
function readExistingConfig(configPath) {
|
|
1040
1277
|
const fallback = {
|
|
1041
1278
|
skills: [],
|
|
1042
1279
|
agents: [],
|
|
1043
1280
|
harness: false
|
|
1044
1281
|
};
|
|
1045
|
-
if (!
|
|
1282
|
+
if (!existsSync11(configPath)) {
|
|
1046
1283
|
return fallback;
|
|
1047
1284
|
}
|
|
1048
1285
|
try {
|
|
@@ -1058,8 +1295,11 @@ function readExistingConfig(configPath) {
|
|
|
1058
1295
|
return fallback;
|
|
1059
1296
|
}
|
|
1060
1297
|
}
|
|
1061
|
-
function writeProjectConfig(targetPath, newSkills, newAgents, harnessEnabled, language) {
|
|
1298
|
+
function writeProjectConfig(targetPath, newSkills, newAgents, harnessEnabled, language, transaction) {
|
|
1062
1299
|
const configPath = getProjectConfigPath(targetPath);
|
|
1300
|
+
if (transaction) {
|
|
1301
|
+
transaction.recordBeforeWrite(configPath);
|
|
1302
|
+
}
|
|
1063
1303
|
const existing = readExistingConfig(configPath);
|
|
1064
1304
|
const mergedSkills = Array.from(/* @__PURE__ */ new Set([...existing.skills, ...newSkills]));
|
|
1065
1305
|
const mergedAgents = Array.from(/* @__PURE__ */ new Set([...existing.agents, ...newAgents]));
|
|
@@ -1073,11 +1313,164 @@ function writeProjectConfig(targetPath, newSkills, newAgents, harnessEnabled, la
|
|
|
1073
1313
|
skills: mergedSkills,
|
|
1074
1314
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1075
1315
|
};
|
|
1076
|
-
|
|
1077
|
-
|
|
1316
|
+
const outcome = safeWriteFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1317
|
+
`);
|
|
1318
|
+
if (outcome.strategy === "replaced" && transaction) {
|
|
1319
|
+
transaction.recordReplacedFile(configPath, outcome.previousOwnerUid);
|
|
1320
|
+
}
|
|
1078
1321
|
return configPath;
|
|
1079
1322
|
}
|
|
1080
1323
|
|
|
1324
|
+
// src/domains/fs/install-transaction.ts
|
|
1325
|
+
import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
|
|
1326
|
+
var InstallTransaction = class {
|
|
1327
|
+
originalFiles = /* @__PURE__ */ new Map();
|
|
1328
|
+
createdDirs = [];
|
|
1329
|
+
replacedFiles = [];
|
|
1330
|
+
recordBeforeWrite(filePath) {
|
|
1331
|
+
if (this.originalFiles.has(filePath)) {
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (existsSync12(filePath)) {
|
|
1335
|
+
try {
|
|
1336
|
+
this.originalFiles.set(filePath, readFileSync7(filePath, "utf-8"));
|
|
1337
|
+
} catch {
|
|
1338
|
+
this.originalFiles.set(filePath, null);
|
|
1339
|
+
}
|
|
1340
|
+
} else {
|
|
1341
|
+
this.originalFiles.set(filePath, null);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
recordCreatedDir(dirPath) {
|
|
1345
|
+
if (!existsSync12(dirPath) && !this.createdDirs.includes(dirPath)) {
|
|
1346
|
+
this.createdDirs.push(dirPath);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
recordReplacedFile(filePath, previousOwnerUid) {
|
|
1350
|
+
if (!this.replacedFiles.some((item) => item.filePath === filePath)) {
|
|
1351
|
+
this.replacedFiles.push({ filePath, previousOwnerUid });
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
getReplacedFiles() {
|
|
1355
|
+
return [...this.replacedFiles];
|
|
1356
|
+
}
|
|
1357
|
+
rollback() {
|
|
1358
|
+
const restoredFiles = [];
|
|
1359
|
+
const removedFiles = [];
|
|
1360
|
+
const removedDirs = [];
|
|
1361
|
+
const failures = [];
|
|
1362
|
+
for (const [filePath, content] of this.originalFiles.entries()) {
|
|
1363
|
+
try {
|
|
1364
|
+
if (content === null) {
|
|
1365
|
+
if (existsSync12(filePath)) {
|
|
1366
|
+
rmSync4(filePath, { force: true });
|
|
1367
|
+
removedFiles.push(filePath);
|
|
1368
|
+
}
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
if (existsSync12(filePath)) {
|
|
1372
|
+
try {
|
|
1373
|
+
const currentContent = readFileSync7(filePath, "utf-8");
|
|
1374
|
+
if (currentContent === content) {
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
} catch {
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
safeWriteFileSync(filePath, content);
|
|
1381
|
+
restoredFiles.push(filePath);
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1384
|
+
failures.push({ path: filePath, reason });
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
const sortedDirs = [...this.createdDirs].sort(
|
|
1388
|
+
(firstDir, secondDir) => secondDir.length - firstDir.length
|
|
1389
|
+
);
|
|
1390
|
+
for (const directoryPath of sortedDirs) {
|
|
1391
|
+
try {
|
|
1392
|
+
if (existsSync12(directoryPath) && readdirSync3(directoryPath).length === 0) {
|
|
1393
|
+
rmSync4(directoryPath, { recursive: true, force: true });
|
|
1394
|
+
removedDirs.push(directoryPath);
|
|
1395
|
+
}
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1398
|
+
failures.push({ path: directoryPath, reason });
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return {
|
|
1402
|
+
restoredFiles,
|
|
1403
|
+
removedFiles,
|
|
1404
|
+
removedDirs,
|
|
1405
|
+
failures,
|
|
1406
|
+
isClean: failures.length === 0
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
// src/domains/fs/preflight-write-check.ts
|
|
1412
|
+
import { join as join11 } from "path";
|
|
1413
|
+
function canRecoverOrWriteTarget(filePath) {
|
|
1414
|
+
const diagnosis = inspectFileAccess(filePath);
|
|
1415
|
+
if (diagnosis.immutableFlag) {
|
|
1416
|
+
throw new FilePermissionError(filePath, diagnosis);
|
|
1417
|
+
}
|
|
1418
|
+
if (!diagnosis.exists) {
|
|
1419
|
+
if (!diagnosis.dirWritable) {
|
|
1420
|
+
throw new FilePermissionError(filePath, diagnosis);
|
|
1421
|
+
}
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
const ownerWritable = diagnosis.ownedByCurrentUser && ((diagnosis.mode ?? 0) & 128) !== 0;
|
|
1425
|
+
if (ownerWritable) {
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (diagnosis.ownedByCurrentUser) {
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
if (diagnosis.dirWritable) {
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
throw new FilePermissionError(filePath, diagnosis);
|
|
1435
|
+
}
|
|
1436
|
+
function runPreflightWriteCheck(options) {
|
|
1437
|
+
const memoryDir = getProjectMemoryDir(options.targetPath);
|
|
1438
|
+
const tasksDir = getProjectTasksDir(options.targetPath);
|
|
1439
|
+
const candidatePaths = [
|
|
1440
|
+
join11(options.targetPath, "AGENTS.md")
|
|
1441
|
+
];
|
|
1442
|
+
if (options.selectedAgents.includes("claude")) {
|
|
1443
|
+
candidatePaths.push(join11(options.targetPath, "CLAUDE.md"));
|
|
1444
|
+
}
|
|
1445
|
+
candidatePaths.push(
|
|
1446
|
+
getProjectConfigPath(options.targetPath),
|
|
1447
|
+
join11(memoryDir, "context.md"),
|
|
1448
|
+
join11(memoryDir, "decisions.md"),
|
|
1449
|
+
join11(tasksDir, "00-onboarding.md"),
|
|
1450
|
+
join11(tasksDir, "_template.md")
|
|
1451
|
+
);
|
|
1452
|
+
for (const filePath of candidatePaths) {
|
|
1453
|
+
canRecoverOrWriteTarget(filePath);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/errors/install-transaction-error.ts
|
|
1458
|
+
var InstallTransactionError = class extends Error {
|
|
1459
|
+
stage;
|
|
1460
|
+
causeError;
|
|
1461
|
+
rollbackReport;
|
|
1462
|
+
suggestedFix;
|
|
1463
|
+
constructor(options) {
|
|
1464
|
+
const rawMessage = options.cause instanceof Error ? options.cause.message : String(options.cause);
|
|
1465
|
+
super(`Error during stage '${options.stage}': ${rawMessage}`);
|
|
1466
|
+
this.name = "InstallTransactionError";
|
|
1467
|
+
this.stage = options.stage;
|
|
1468
|
+
this.causeError = options.cause;
|
|
1469
|
+
this.rollbackReport = options.rollbackReport;
|
|
1470
|
+
this.suggestedFix = options.suggestedFix;
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1081
1474
|
// src/domains/installer/install-orchestrator.ts
|
|
1082
1475
|
var ADAPTER_REGISTRY = {
|
|
1083
1476
|
antigravity: new AntigravityAdapter(),
|
|
@@ -1085,7 +1478,20 @@ var ADAPTER_REGISTRY = {
|
|
|
1085
1478
|
cursor: new CursorAdapter(),
|
|
1086
1479
|
gemini: new GeminiAdapter()
|
|
1087
1480
|
};
|
|
1481
|
+
function formatSuggestedFix(cause) {
|
|
1482
|
+
if (cause instanceof FilePermissionError) {
|
|
1483
|
+
return cause.suggestedFixCommand;
|
|
1484
|
+
}
|
|
1485
|
+
const nodeError = cause;
|
|
1486
|
+
if (nodeError?.code === "EACCES" || nodeError?.code === "EPERM") {
|
|
1487
|
+
if (nodeError.path) {
|
|
1488
|
+
return `chmod u+w "${nodeError.path}"`;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return void 0;
|
|
1492
|
+
}
|
|
1088
1493
|
async function installSkills(options) {
|
|
1494
|
+
const transaction = new InstallTransaction();
|
|
1089
1495
|
const skillsToInstall = [...options.selectedSkillIds];
|
|
1090
1496
|
if (!skillsToInstall.includes("groove-harness")) {
|
|
1091
1497
|
skillsToInstall.push("groove-harness");
|
|
@@ -1093,62 +1499,90 @@ async function installSkills(options) {
|
|
|
1093
1499
|
if (!skillsToInstall.includes("odd-ai")) {
|
|
1094
1500
|
skillsToInstall.push("odd-ai");
|
|
1095
1501
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1502
|
+
let currentStage = "Preflight check";
|
|
1503
|
+
try {
|
|
1504
|
+
runPreflightWriteCheck({
|
|
1505
|
+
targetPath: options.targetPath,
|
|
1506
|
+
selectedAgents: options.selectedAgents
|
|
1507
|
+
});
|
|
1508
|
+
currentStage = "Syncing skills store (.grooveskills/skills)";
|
|
1509
|
+
syncProjectStore(options.targetPath, skillsToInstall, options.selectedAgents, transaction);
|
|
1510
|
+
const projectSkillsDir = getProjectSkillsDir(options.targetPath);
|
|
1511
|
+
const createdFiles = [];
|
|
1512
|
+
currentStage = "Configuring agent references (.agents, .claude, etc.)";
|
|
1513
|
+
for (const skillId of skillsToInstall) {
|
|
1514
|
+
const skill = SKILL_CATALOG.find((catalogItem) => catalogItem.id === skillId);
|
|
1515
|
+
if (!skill) {
|
|
1108
1516
|
continue;
|
|
1109
1517
|
}
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1518
|
+
const skillSourceDir = join12(projectSkillsDir, skill.directoryName);
|
|
1519
|
+
for (const agentId of options.selectedAgents) {
|
|
1520
|
+
const adapter = ADAPTER_REGISTRY[agentId];
|
|
1521
|
+
if (!adapter) {
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
const files = await adapter.installSkill(
|
|
1525
|
+
options.targetPath,
|
|
1526
|
+
skill,
|
|
1527
|
+
skillSourceDir,
|
|
1528
|
+
transaction
|
|
1529
|
+
);
|
|
1530
|
+
createdFiles.push(...files);
|
|
1531
|
+
}
|
|
1112
1532
|
}
|
|
1533
|
+
currentStage = "Installing Groove Harness (context, decisions, tasks, and instructions)";
|
|
1534
|
+
const harnessResult = installHarness({
|
|
1535
|
+
targetPath: options.targetPath,
|
|
1536
|
+
selectedAgents: options.selectedAgents,
|
|
1537
|
+
selectedSkillIds: skillsToInstall,
|
|
1538
|
+
detectedDependencies: options.detectedDependencies || [],
|
|
1539
|
+
language: options.language,
|
|
1540
|
+
forceOnboarding: options.forceOnboarding,
|
|
1541
|
+
transaction
|
|
1542
|
+
});
|
|
1543
|
+
createdFiles.push(...harnessResult.createdFiles);
|
|
1544
|
+
currentStage = "Persisting project configuration (.grooveskills/config.json)";
|
|
1545
|
+
const configPath = writeProjectConfig(
|
|
1546
|
+
options.targetPath,
|
|
1547
|
+
skillsToInstall,
|
|
1548
|
+
options.selectedAgents,
|
|
1549
|
+
true,
|
|
1550
|
+
options.language,
|
|
1551
|
+
transaction
|
|
1552
|
+
);
|
|
1553
|
+
createdFiles.push(configPath);
|
|
1554
|
+
return {
|
|
1555
|
+
installedSkills: skillsToInstall,
|
|
1556
|
+
configuredAgents: options.selectedAgents,
|
|
1557
|
+
createdFiles,
|
|
1558
|
+
harnessInitialized: harnessResult.onboardingInitialized,
|
|
1559
|
+
language: options.language,
|
|
1560
|
+
replacedFiles: transaction.getReplacedFiles()
|
|
1561
|
+
};
|
|
1562
|
+
} catch (cause) {
|
|
1563
|
+
const rollbackReport = transaction.rollback();
|
|
1564
|
+
const suggestedFix = formatSuggestedFix(cause);
|
|
1565
|
+
throw new InstallTransactionError({
|
|
1566
|
+
stage: currentStage,
|
|
1567
|
+
cause,
|
|
1568
|
+
rollbackReport,
|
|
1569
|
+
suggestedFix
|
|
1570
|
+
});
|
|
1113
1571
|
}
|
|
1114
|
-
const harnessResult = installHarness({
|
|
1115
|
-
targetPath: options.targetPath,
|
|
1116
|
-
selectedAgents: options.selectedAgents,
|
|
1117
|
-
selectedSkillIds: skillsToInstall,
|
|
1118
|
-
detectedDependencies: options.detectedDependencies || [],
|
|
1119
|
-
language: options.language,
|
|
1120
|
-
forceOnboarding: options.forceOnboarding
|
|
1121
|
-
});
|
|
1122
|
-
createdFiles.push(...harnessResult.createdFiles);
|
|
1123
|
-
const configPath = writeProjectConfig(
|
|
1124
|
-
options.targetPath,
|
|
1125
|
-
skillsToInstall,
|
|
1126
|
-
options.selectedAgents,
|
|
1127
|
-
true,
|
|
1128
|
-
options.language
|
|
1129
|
-
);
|
|
1130
|
-
createdFiles.push(configPath);
|
|
1131
|
-
return {
|
|
1132
|
-
installedSkills: skillsToInstall,
|
|
1133
|
-
configuredAgents: options.selectedAgents,
|
|
1134
|
-
createdFiles,
|
|
1135
|
-
harnessInitialized: harnessResult.onboardingInitialized,
|
|
1136
|
-
language: options.language
|
|
1137
|
-
};
|
|
1138
1572
|
}
|
|
1139
1573
|
|
|
1140
1574
|
// src/domains/ui/render-onboarding-prompt.ts
|
|
1141
|
-
import
|
|
1575
|
+
import picocolors2 from "picocolors";
|
|
1142
1576
|
function formatOnboardingPrompt(language = "es") {
|
|
1143
|
-
const
|
|
1144
|
-
const title =
|
|
1145
|
-
const prompt =
|
|
1146
|
-
const separator =
|
|
1577
|
+
const languageConfig = SUPPORTED_LANGUAGES[language] ?? SUPPORTED_LANGUAGES.es;
|
|
1578
|
+
const title = languageConfig.onboardingPromptTitle;
|
|
1579
|
+
const prompt = languageConfig.onboardingPrompt;
|
|
1580
|
+
const separator = picocolors2.cyan("\u2500".repeat(60));
|
|
1147
1581
|
const formattedOutput = [
|
|
1148
1582
|
separator,
|
|
1149
|
-
|
|
1583
|
+
picocolors2.bold(picocolors2.yellow(title)),
|
|
1150
1584
|
"",
|
|
1151
|
-
|
|
1585
|
+
picocolors2.green(prompt),
|
|
1152
1586
|
separator
|
|
1153
1587
|
].join("\n");
|
|
1154
1588
|
return {
|
|
@@ -1158,53 +1592,130 @@ function formatOnboardingPrompt(language = "es") {
|
|
|
1158
1592
|
};
|
|
1159
1593
|
}
|
|
1160
1594
|
|
|
1595
|
+
// src/domains/ui/render-install-report.ts
|
|
1596
|
+
import { basename as basename2 } from "path";
|
|
1597
|
+
import { userInfo } from "os";
|
|
1598
|
+
import * as prompts5 from "@clack/prompts";
|
|
1599
|
+
import picocolors3 from "picocolors";
|
|
1600
|
+
function getCurrentUsername() {
|
|
1601
|
+
try {
|
|
1602
|
+
return userInfo().username;
|
|
1603
|
+
} catch {
|
|
1604
|
+
return process.env.USER || process.env.LOGNAME || "current user";
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function formatReplacedFileNotes(replacedFiles = []) {
|
|
1608
|
+
if (replacedFiles.length === 0) {
|
|
1609
|
+
return [];
|
|
1610
|
+
}
|
|
1611
|
+
const currentUser = getCurrentUsername();
|
|
1612
|
+
return replacedFiles.map((replacedItem) => {
|
|
1613
|
+
const fileName = basename2(replacedItem.filePath);
|
|
1614
|
+
const previousOwner = formatOwnerName(replacedItem.previousOwnerUid);
|
|
1615
|
+
return picocolors3.yellow(
|
|
1616
|
+
`Note: ${fileName} was owned by ${previousOwner}. It was replaced atomically and is now owned by ${currentUser}.`
|
|
1617
|
+
);
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
function renderInstallFailure(error) {
|
|
1621
|
+
const { stage, causeError, rollbackReport, suggestedFix } = error;
|
|
1622
|
+
const causeMessage = causeError instanceof Error ? causeError.message : String(causeError);
|
|
1623
|
+
const nothingTouched = rollbackReport.restoredFiles.length === 0 && rollbackReport.removedFiles.length === 0 && rollbackReport.removedDirs.length === 0 && rollbackReport.failures.length === 0;
|
|
1624
|
+
const lines = [
|
|
1625
|
+
`${picocolors3.bold("Failed stage:")} ${picocolors3.yellow(stage)}`,
|
|
1626
|
+
`${picocolors3.bold("Cause:")} ${picocolors3.red(causeMessage)}`
|
|
1627
|
+
];
|
|
1628
|
+
if (nothingTouched) {
|
|
1629
|
+
lines.push(`${picocolors3.bold("Rollback:")} Nothing to roll back, no files were modified.`);
|
|
1630
|
+
} else {
|
|
1631
|
+
lines.push(
|
|
1632
|
+
"",
|
|
1633
|
+
picocolors3.bold("Rollback summary:"),
|
|
1634
|
+
` \u2022 Restored files: ${picocolors3.cyan(String(rollbackReport.restoredFiles.length))}`,
|
|
1635
|
+
` \u2022 Removed temporary files: ${picocolors3.cyan(String(rollbackReport.removedFiles.length))}`,
|
|
1636
|
+
` \u2022 Removed empty directories: ${picocolors3.cyan(String(rollbackReport.removedDirs.length))}`
|
|
1637
|
+
);
|
|
1638
|
+
if (!rollbackReport.isClean) {
|
|
1639
|
+
lines.push("", picocolors3.red("Rollback was incomplete. These files may need manual review:"));
|
|
1640
|
+
for (const failure of rollbackReport.failures) {
|
|
1641
|
+
lines.push(` \u2022 ${failure.path} (${failure.reason})`);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
if (suggestedFix) {
|
|
1646
|
+
lines.push(`${picocolors3.bold("Suggested fix:")} ${picocolors3.green(suggestedFix)}`);
|
|
1647
|
+
}
|
|
1648
|
+
prompts5.note(lines.join("\n"), "Installation Aborted");
|
|
1649
|
+
if (!rollbackReport.isClean) {
|
|
1650
|
+
prompts5.outro(
|
|
1651
|
+
picocolors3.red(
|
|
1652
|
+
"Rollback was incomplete. Please review the files listed above and apply the suggested fix."
|
|
1653
|
+
)
|
|
1654
|
+
);
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
prompts5.outro(
|
|
1658
|
+
picocolors3.yellow(
|
|
1659
|
+
nothingTouched ? "No files were modified. Apply the suggested fix and try again." : "Your project was restored to its previous state. Apply the suggested fix and try again."
|
|
1660
|
+
)
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1161
1664
|
// src/domains/ui/ui-orchestrator.ts
|
|
1162
1665
|
async function resolveLanguageSelection(harnessExists, configuredLanguage, forceOnboarding) {
|
|
1163
1666
|
if (harnessExists && !forceOnboarding) {
|
|
1164
1667
|
return configuredLanguage ?? "es";
|
|
1165
1668
|
}
|
|
1166
|
-
const
|
|
1167
|
-
if (
|
|
1168
|
-
|
|
1669
|
+
const languageSelection = await promptLanguage(configuredLanguage ?? "es");
|
|
1670
|
+
if (prompts6.isCancel(languageSelection)) {
|
|
1671
|
+
prompts6.cancel("Installation cancelled. Have a wonderful day!");
|
|
1169
1672
|
process.exit(0);
|
|
1170
1673
|
}
|
|
1171
|
-
return
|
|
1674
|
+
return languageSelection;
|
|
1172
1675
|
}
|
|
1173
1676
|
async function resolveOverwriteConfirmation(targetPath, selectedSkills, selectedAgents) {
|
|
1174
1677
|
const anyExists = selectedSkills.some((skillId) => {
|
|
1175
|
-
const
|
|
1176
|
-
return
|
|
1678
|
+
const skillMetadata = SKILL_CATALOG.find((skill) => skill.id === skillId);
|
|
1679
|
+
return skillMetadata ? verifySkillExists(targetPath, skillMetadata.directoryName, selectedAgents) : false;
|
|
1177
1680
|
});
|
|
1178
1681
|
if (!anyExists) {
|
|
1179
1682
|
return true;
|
|
1180
1683
|
}
|
|
1181
|
-
const
|
|
1182
|
-
if (
|
|
1183
|
-
|
|
1684
|
+
const overwriteConfirmed = await promptConfirmOverwrite();
|
|
1685
|
+
if (prompts6.isCancel(overwriteConfirmed) || !overwriteConfirmed) {
|
|
1686
|
+
prompts6.cancel("Installation stopped to protect existing skills.");
|
|
1184
1687
|
process.exit(0);
|
|
1185
1688
|
}
|
|
1186
|
-
return Boolean(
|
|
1689
|
+
return Boolean(overwriteConfirmed);
|
|
1187
1690
|
}
|
|
1188
1691
|
async function runInteractiveUi(context, forceOnboarding = false) {
|
|
1189
1692
|
console.log(renderBanner());
|
|
1190
|
-
|
|
1693
|
+
prompts6.intro(
|
|
1694
|
+
picocolors4.magenta(
|
|
1695
|
+
"Welcome to GrooveSkills! Let's tailor your AI agents with clean architectural standards."
|
|
1696
|
+
)
|
|
1697
|
+
);
|
|
1191
1698
|
if (context.detectedDependencies.length > 0) {
|
|
1192
|
-
const harnessInfo = context.harnessExists && !forceOnboarding ?
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1699
|
+
const harnessInfo = context.harnessExists && !forceOnboarding ? picocolors4.green(
|
|
1700
|
+
`Active (.grooveskills/memory, lang: ${context.configuredLanguage ?? "es"})`
|
|
1701
|
+
) : picocolors4.cyan("Will be initialized");
|
|
1702
|
+
const formattedDependencies = context.detectedDependencies.map((dependencyName) => picocolors4.bold(dependencyName)).join(", ");
|
|
1703
|
+
const formattedAgents = context.detectedAgents.map((agentName) => picocolors4.green(agentName)).join(", ") || "None (will be created)";
|
|
1704
|
+
prompts6.note(
|
|
1705
|
+
`Detected dependencies: ${formattedDependencies}
|
|
1706
|
+
Active AI folders: ${formattedAgents}
|
|
1196
1707
|
GrooveSkills Harness: ${harnessInfo}`,
|
|
1197
1708
|
"Project Environment"
|
|
1198
1709
|
);
|
|
1199
1710
|
}
|
|
1200
1711
|
const selectedSkills = await promptSkills(context.suggestedSkillIds);
|
|
1201
|
-
if (
|
|
1202
|
-
|
|
1712
|
+
if (prompts6.isCancel(selectedSkills)) {
|
|
1713
|
+
prompts6.cancel("Installation cancelled. Have a wonderful day!");
|
|
1203
1714
|
process.exit(0);
|
|
1204
1715
|
}
|
|
1205
1716
|
const selectedAgents = await promptAgents(context.detectedAgents);
|
|
1206
|
-
if (
|
|
1207
|
-
|
|
1717
|
+
if (prompts6.isCancel(selectedAgents)) {
|
|
1718
|
+
prompts6.cancel("Installation cancelled. Have a wonderful day!");
|
|
1208
1719
|
process.exit(0);
|
|
1209
1720
|
}
|
|
1210
1721
|
const selectedLanguage = await resolveLanguageSelection(
|
|
@@ -1217,37 +1728,59 @@ GrooveSkills Harness: ${harnessInfo}`,
|
|
|
1217
1728
|
selectedSkills,
|
|
1218
1729
|
selectedAgents
|
|
1219
1730
|
);
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
"
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1731
|
+
const installSpinner = prompts6.spinner();
|
|
1732
|
+
installSpinner.start("Installing and adapting skills across your configured AI agents...");
|
|
1733
|
+
try {
|
|
1734
|
+
const installResult = await installSkills({
|
|
1735
|
+
targetPath: context.targetPath,
|
|
1736
|
+
selectedSkillIds: selectedSkills,
|
|
1737
|
+
selectedAgents,
|
|
1738
|
+
forceOverwrite,
|
|
1739
|
+
enableHarness: true,
|
|
1740
|
+
detectedDependencies: context.detectedDependencies,
|
|
1741
|
+
language: selectedLanguage,
|
|
1742
|
+
forceOnboarding
|
|
1743
|
+
});
|
|
1744
|
+
installSpinner.stop(
|
|
1745
|
+
picocolors4.green("All skills and harness have been successfully configured!")
|
|
1746
|
+
);
|
|
1747
|
+
const harnessStatus = installResult.harnessInitialized ? picocolors4.green(`Initialized in .grooveskills/ (Language: ${selectedLanguage})`) : picocolors4.cyan(`Preserved in .grooveskills/ (Language: ${selectedLanguage})`);
|
|
1748
|
+
const formattedInstalledSkills = installResult.installedSkills.map((skillId) => picocolors4.bold(skillId)).join(", ");
|
|
1749
|
+
const formattedConfiguredAgents = installResult.configuredAgents.map((agentId) => picocolors4.cyan(agentId)).join(", ");
|
|
1750
|
+
const summaryLines = [
|
|
1751
|
+
`Configured skills: ${formattedInstalledSkills}`,
|
|
1752
|
+
`Target agents: ${formattedConfiguredAgents}`,
|
|
1753
|
+
`GrooveSkills Harness: ${harnessStatus}`,
|
|
1754
|
+
...formatReplacedFileNotes(installResult.replacedFiles)
|
|
1755
|
+
];
|
|
1756
|
+
prompts6.note(summaryLines.join("\n"), "Installation Summary");
|
|
1757
|
+
if (installResult.harnessInitialized) {
|
|
1758
|
+
const promptRender = formatOnboardingPrompt(selectedLanguage);
|
|
1759
|
+
prompts6.note(picocolors4.green(promptRender.prompt), promptRender.title);
|
|
1760
|
+
}
|
|
1761
|
+
prompts6.outro(
|
|
1762
|
+
picocolors4.green(
|
|
1763
|
+
"Your AI programming pair is now empowered with your team's architectural habits! \u{1F680}"
|
|
1764
|
+
)
|
|
1245
1765
|
);
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
installSpinner.stop(picocolors4.yellow("Installation could not be completed."));
|
|
1768
|
+
if (error instanceof InstallTransactionError) {
|
|
1769
|
+
renderInstallFailure(error);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
throw error;
|
|
1246
1773
|
}
|
|
1247
|
-
p5.outro(pc3.green("Your AI programming pair is now empowered with your team's architectural habits! \u{1F680}"));
|
|
1248
1774
|
}
|
|
1249
1775
|
|
|
1250
1776
|
// src/main.ts
|
|
1777
|
+
function resolveUsername() {
|
|
1778
|
+
try {
|
|
1779
|
+
return userInfo2().username;
|
|
1780
|
+
} catch {
|
|
1781
|
+
return process.env.USER || process.env.LOGNAME || "current user";
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1251
1784
|
async function runGrooveSkills(options = {}) {
|
|
1252
1785
|
const targetPath = process.cwd();
|
|
1253
1786
|
const context = detectProjectContext(targetPath);
|
|
@@ -1267,6 +1800,18 @@ async function runGrooveSkills(options = {}) {
|
|
|
1267
1800
|
language,
|
|
1268
1801
|
forceOnboarding: options.forceOnboarding
|
|
1269
1802
|
});
|
|
1803
|
+
if (result.replacedFiles && result.replacedFiles.length > 0) {
|
|
1804
|
+
const currentUser = resolveUsername();
|
|
1805
|
+
for (const replacedItem of result.replacedFiles) {
|
|
1806
|
+
const fileName = basename3(replacedItem.filePath);
|
|
1807
|
+
const previousOwner = formatOwnerName(replacedItem.previousOwnerUid);
|
|
1808
|
+
console.log(
|
|
1809
|
+
picocolors5.yellow(
|
|
1810
|
+
`Note: ${fileName} was owned by ${previousOwner}. It was replaced atomically and is now owned by ${currentUser}.`
|
|
1811
|
+
)
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1270
1815
|
if (result.harnessInitialized) {
|
|
1271
1816
|
const promptRender = formatOnboardingPrompt(language);
|
|
1272
1817
|
console.log(`
|
|
@@ -1279,6 +1824,7 @@ ${promptRender.formattedOutput}
|
|
|
1279
1824
|
}
|
|
1280
1825
|
|
|
1281
1826
|
export {
|
|
1827
|
+
InstallTransactionError,
|
|
1282
1828
|
runGrooveSkills
|
|
1283
1829
|
};
|
|
1284
|
-
//# sourceMappingURL=chunk-
|
|
1830
|
+
//# sourceMappingURL=chunk-SP3ESDM5.js.map
|