@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.
@@ -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 dep of Object.keys(parsed.dependencies ?? {})) {
41
- detected.add(dep);
45
+ for (const dependencyName of Object.keys(parsed.dependencies ?? {})) {
46
+ detected.add(dependencyName);
42
47
  }
43
- for (const dep of Object.keys(parsed.devDependencies ?? {})) {
44
- detected.add(dep);
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((dep) => dependencies.includes(dep));
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 p5 from "@clack/prompts";
280
- import pc3 from "picocolors";
294
+ import * as prompts6 from "@clack/prompts";
295
+ import picocolors4 from "picocolors";
281
296
 
282
297
  // src/domains/ui/banners.ts
283
- import pc from "picocolors";
298
+ import picocolors from "picocolors";
284
299
  function renderBanner() {
285
- const title = pc.bold(pc.white(" GROOVE SKILLS "));
286
- const badge = pc.bgMagenta(pc.black(title));
287
- const subtitle = pc.dim("Multi-Agent AI Skills & Architectural Standards");
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 p from "@clack/prompts";
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 p.multiselect({
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 p2 from "@clack/prompts";
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 p2.multiselect({
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 p3 from "@clack/prompts";
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: "Recomendado para proyectos en espa\xF1ol" },
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: "Portugu\xEAs para o Brasil e Portugal" },
339
- { value: "it", label: "Italiano (Italian)", hint: "Comunicazione in lingua italiana" },
340
- { value: "fr", label: "Fran\xE7ais (French)", hint: "Communication en langue fran\xE7aise" }
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 p3.select({
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 p4 from "@clack/prompts";
366
+ import * as prompts4 from "@clack/prompts";
352
367
  async function promptConfirmOverwrite() {
353
- return await p4.confirm({
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: (dir) => join3(".agents", "skills", dir),
364
- claude: (dir) => join3(".claude", "skills", dir),
365
- cursor: (dir) => join3(".cursor", "rules", `${dir}.mdc`),
366
- gemini: (dir) => join3(".gemini", "skills", dir)
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 join11 } from "path";
397
+ import { join as join12 } from "path";
383
398
 
384
399
  // src/domains/adapters/skill-reference-builder.ts
385
- import { existsSync as existsSync3, lstatSync as lstatSync2, mkdirSync, readFileSync as readFileSync3, rmSync, unlinkSync, writeFileSync } from "fs";
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 = existsSync3(path);
606
+ const fileExists = existsSync5(path);
409
607
  if (!fileExists) {
410
608
  try {
411
- const lstat = lstatSync2(path);
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 = lstatSync2(path);
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
- rmSync(path, { recursive: true, force: true });
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 (!existsSync3(canonicalSkillMdPath)) {
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
- const targetFile = join5(agentSkillFolder, "SKILL.md");
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
- if (!existsSync3(rulesDir)) {
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
- writeFileSync(targetFile, referenceContent, "utf-8");
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 existsSync5, mkdirSync as mkdirSync2, rmSync as rmSync2 } from "fs";
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 existsSync4 } from "fs";
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 = dirname(currentFilePath);
761
+ const currentDir = dirname2(currentFilePath);
555
762
  const candidateFromDist = resolve(currentDir, "../skills");
556
- if (existsSync4(candidateFromDist)) {
763
+ if (existsSync6(candidateFromDist)) {
557
764
  return candidateFromDist;
558
765
  }
559
766
  const candidateFromSrc = resolve(currentDir, "../../../skills");
560
- if (existsSync4(candidateFromSrc)) {
767
+ if (existsSync6(candidateFromSrc)) {
561
768
  return candidateFromSrc;
562
769
  }
563
770
  let searchDir = currentDir;
564
- for (let i = 0; i < 5; i++) {
771
+ for (let depth = 0; depth < 5; depth++) {
565
772
  const candidate = join6(searchDir, "skills");
566
- if (existsSync4(candidate)) {
773
+ if (existsSync6(candidate)) {
567
774
  return candidate;
568
775
  }
569
- const parent = dirname(searchDir);
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 (!existsSync5(projectSkillsDir)) {
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 (existsSync5(legacyCursorRules)) {
798
+ if (existsSync7(legacyCursorRules)) {
589
799
  try {
590
- rmSync2(legacyCursorRules, { recursive: true, force: true });
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 (existsSync5(sourceSkillDir)) {
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 existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
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 depsFormatted = detectedDependencies.length > 0 ? detectedDependencies.map((d) => `- \`${d}\``).join("\n") : "- (No external dependencies detected)";
658
- const langConfig = SUPPORTED_LANGUAGES[language];
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 pkgManager = profile?.packageManager ?? "npm";
873
+ const packageManager = profile?.packageManager ?? "npm";
661
874
  const commandLines = [];
662
875
  if (profile?.commands) {
663
- for (const [key, cmd] of Object.entries(profile.commands)) {
664
- commandLines.push(`- **${key}**: \`${cmd}\``);
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 [alias, target] of Object.entries(profile.pathAliases)) {
673
- aliasLines.push(`- \`${alias}\` -> \`${target}\``);
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 dirLines = [];
893
+ const directoryLines = [];
681
894
  if (profile?.sourceDirectories && profile.sourceDirectories.length > 0) {
682
- for (const dir of profile.sourceDirectories) {
683
- dirLines.push(`- \`${dir}/\``);
895
+ for (const directoryName of profile.sourceDirectories) {
896
+ directoryLines.push(`- \`${directoryName}/\``);
684
897
  }
685
898
  }
686
- const dirsSection = dirLines.length > 0 ? `
899
+ const directoriesSection = directoryLines.length > 0 ? `
687
900
  ### Source Modules
688
- ${dirLines.join("\n")}
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**: \`${pkgManager}\`
696
- ${depsFormatted}
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}${dirsSection}
915
+ ${aliasesSection}${directoriesSection}
703
916
  ## 3. Communication & User Copy Language
704
- - **Configured Language**: ${langConfig.nativeName} (\`${language}\`).
705
- - **Policy**: ${langConfig.instruction}
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 now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
713
- const langConfig = SUPPORTED_LANGUAGES[language];
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
- | ${now} | Adopt GrooveSkills Harness | Initialized \`.grooveskills/\` control plane with ODD | Clean, encapsulated agent memory, 3-lane ODD flow, and ${langConfig.nativeName} copy standard |
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 now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
937
+ const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
725
938
  return `---
726
939
  status: pending
727
- createdAt: ${now}
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 langConfig = SUPPORTED_LANGUAGES[language];
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 (${langConfig.nativeName})
797
- - **Language**: \`${language}\` (${langConfig.nativeName}).
798
- - **Requirement**: ${langConfig.instruction}
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 existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
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 (existsSync6(packageJsonPath)) {
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 (existsSync6(cargoPath)) {
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 (existsSync6(pyprojectPath)) {
1047
+ if (existsSync8(pyprojectPath)) {
835
1048
  try {
836
- const pyRaw = readFileSync4(pyprojectPath, "utf-8");
837
- const pyMatch = pyRaw.match(/name\s*=\s*"([^"]+)"/);
838
- if (pyMatch?.[1]) {
839
- return pyMatch[1];
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 (existsSync6(join8(targetPath, "pnpm-lock.yaml"))) {
1060
+ if (existsSync8(join8(targetPath, "pnpm-lock.yaml"))) {
848
1061
  return "pnpm";
849
1062
  }
850
- if (existsSync6(join8(targetPath, "bun.lockb")) || existsSync6(join8(targetPath, "bun.lock"))) {
1063
+ if (existsSync8(join8(targetPath, "bun.lockb")) || existsSync8(join8(targetPath, "bun.lock"))) {
851
1064
  return "bun";
852
1065
  }
853
- if (existsSync6(join8(targetPath, "yarn.lock"))) {
1066
+ if (existsSync8(join8(targetPath, "yarn.lock"))) {
854
1067
  return "yarn";
855
1068
  }
856
- if (existsSync6(join8(targetPath, "package-lock.json"))) {
1069
+ if (existsSync8(join8(targetPath, "package-lock.json"))) {
857
1070
  return "npm";
858
1071
  }
859
- if (existsSync6(join8(targetPath, "Cargo.lock")) || existsSync6(join8(targetPath, "Cargo.toml"))) {
1072
+ if (existsSync8(join8(targetPath, "Cargo.lock")) || existsSync8(join8(targetPath, "Cargo.toml"))) {
860
1073
  return "cargo";
861
1074
  }
862
- if (existsSync6(join8(targetPath, "uv.lock"))) {
1075
+ if (existsSync8(join8(targetPath, "uv.lock"))) {
863
1076
  return "uv";
864
1077
  }
865
- if (existsSync6(join8(targetPath, "poetry.lock"))) {
1078
+ if (existsSync8(join8(targetPath, "poetry.lock"))) {
866
1079
  return "poetry";
867
1080
  }
868
1081
  return "npm";
869
1082
  }
870
- function detectCommands(targetPath, pkgManager) {
1083
+ function detectCommands(targetPath, packageManager) {
871
1084
  const packageJsonPath = join8(targetPath, "package.json");
872
- if (!existsSync6(packageJsonPath)) {
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 = pkgManager === "npm" ? "npm run" : pkgManager;
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 (!existsSync6(tsconfigPath)) {
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 [key, targets] of Object.entries(paths)) {
916
- if (Array.isArray(targets) && typeof targets[0] === "string") {
917
- aliases[key] = targets[0];
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 = existsSync6(srcPath) ? srcPath : targetPath;
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 existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
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 applyHarnessBlock(filePath, language = "es") {
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 (!existsSync7(filePath)) {
958
- writeFileSync2(filePath, `${block}
959
- `, "utf-8");
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
- writeFileSync2(filePath, updated, "utf-8");
1188
+ writeWithTransaction(filePath, updated, transaction);
967
1189
  return;
968
1190
  }
969
1191
  const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
970
- writeFileSync2(filePath, `${existingContent}${separator}${block}
971
- `, "utf-8");
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
- if (!existsSync8(tasksDir)) {
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 = !existsSync8(contextPath) || forceOnboarding;
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
- writeFileSync3(contextPath, contextContent, "utf-8");
1241
+ writeTrackedFile(contextPath, contextContent, transaction);
1006
1242
  createdFiles.push(contextPath);
1007
1243
  const onboardingTaskPath = join10(tasksDir, "00-onboarding.md");
1008
- writeFileSync3(
1244
+ writeTrackedFile(
1009
1245
  onboardingTaskPath,
1010
1246
  buildOnboardingTaskTemplate(profile.projectName),
1011
- "utf-8"
1247
+ transaction
1012
1248
  );
1013
1249
  createdFiles.push(onboardingTaskPath);
1014
1250
  }
1015
1251
  const decisionsPath = join10(memoryDir, "decisions.md");
1016
- if (!existsSync8(decisionsPath)) {
1017
- writeFileSync3(decisionsPath, buildDecisionsTemplate(language), "utf-8");
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 (!existsSync8(templateTaskPath)) {
1022
- writeFileSync3(templateTaskPath, buildTaskTemplate(), "utf-8");
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 existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
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 (!existsSync9(configPath)) {
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
- writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
1077
- `, "utf-8");
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
- syncProjectStore(options.targetPath, skillsToInstall, options.selectedAgents);
1097
- const projectSkillsDir = getProjectSkillsDir(options.targetPath);
1098
- const createdFiles = [];
1099
- for (const skillId of skillsToInstall) {
1100
- const skill = SKILL_CATALOG.find((item) => item.id === skillId);
1101
- if (!skill) {
1102
- continue;
1103
- }
1104
- const skillSourceDir = join11(projectSkillsDir, skill.directoryName);
1105
- for (const agentId of options.selectedAgents) {
1106
- const adapter = ADAPTER_REGISTRY[agentId];
1107
- if (!adapter) {
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 files = await adapter.installSkill(options.targetPath, skill, skillSourceDir);
1111
- createdFiles.push(...files);
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 pc2 from "picocolors";
1575
+ import picocolors2 from "picocolors";
1142
1576
  function formatOnboardingPrompt(language = "es") {
1143
- const langConfig = SUPPORTED_LANGUAGES[language] ?? SUPPORTED_LANGUAGES.es;
1144
- const title = langConfig.onboardingPromptTitle;
1145
- const prompt = langConfig.onboardingPrompt;
1146
- const separator = pc2.cyan("\u2500".repeat(60));
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
- pc2.bold(pc2.yellow(title)),
1583
+ picocolors2.bold(picocolors2.yellow(title)),
1150
1584
  "",
1151
- pc2.green(prompt),
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 langChoice = await promptLanguage(configuredLanguage ?? "es");
1167
- if (p5.isCancel(langChoice)) {
1168
- p5.cancel("Installation cancelled. Have a wonderful day!");
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 langChoice;
1674
+ return languageSelection;
1172
1675
  }
1173
1676
  async function resolveOverwriteConfirmation(targetPath, selectedSkills, selectedAgents) {
1174
1677
  const anyExists = selectedSkills.some((skillId) => {
1175
- const meta = SKILL_CATALOG.find((s) => s.id === skillId);
1176
- return meta ? verifySkillExists(targetPath, meta.directoryName, selectedAgents) : false;
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 confirm2 = await promptConfirmOverwrite();
1182
- if (p5.isCancel(confirm2) || !confirm2) {
1183
- p5.cancel("Installation stopped to protect existing skills.");
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(confirm2);
1689
+ return Boolean(overwriteConfirmed);
1187
1690
  }
1188
1691
  async function runInteractiveUi(context, forceOnboarding = false) {
1189
1692
  console.log(renderBanner());
1190
- p5.intro(pc3.magenta("Welcome to GrooveSkills! Let's tailor your AI agents with clean architectural standards."));
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 ? pc3.green(`Active (.grooveskills/memory, lang: ${context.configuredLanguage ?? "es"})`) : pc3.cyan("Will be initialized");
1193
- p5.note(
1194
- `Detected dependencies: ${context.detectedDependencies.map((d) => pc3.bold(d)).join(", ")}
1195
- Active AI folders: ${context.detectedAgents.map((a) => pc3.green(a)).join(", ") || "None (will be created)"}
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 (p5.isCancel(selectedSkills)) {
1202
- p5.cancel("Installation cancelled. Have a wonderful day!");
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 (p5.isCancel(selectedAgents)) {
1207
- p5.cancel("Installation cancelled. Have a wonderful day!");
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 s = p5.spinner();
1221
- s.start("Installing and adapting skills across your configured AI agents...");
1222
- const result = await installSkills({
1223
- targetPath: context.targetPath,
1224
- selectedSkillIds: selectedSkills,
1225
- selectedAgents,
1226
- forceOverwrite,
1227
- enableHarness: true,
1228
- detectedDependencies: context.detectedDependencies,
1229
- language: selectedLanguage,
1230
- forceOnboarding
1231
- });
1232
- s.stop(pc3.green("All skills and harness have been successfully configured!"));
1233
- const harnessStatus = result.harnessInitialized ? pc3.green(`Initialized in .grooveskills/ (Language: ${selectedLanguage})`) : pc3.cyan(`Preserved in .grooveskills/ (Language: ${selectedLanguage})`);
1234
- p5.note(
1235
- `Configured skills: ${result.installedSkills.map((s2) => pc3.bold(s2)).join(", ")}
1236
- Target agents: ${result.configuredAgents.map((a) => pc3.cyan(a)).join(", ")}
1237
- GrooveSkills Harness: ${harnessStatus}`,
1238
- "Installation Summary"
1239
- );
1240
- if (result.harnessInitialized) {
1241
- const promptRender = formatOnboardingPrompt(selectedLanguage);
1242
- p5.note(
1243
- pc3.green(promptRender.prompt),
1244
- promptRender.title
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-HUG3MG3P.js.map
1830
+ //# sourceMappingURL=chunk-SP3ESDM5.js.map