@3lineas/grooveskills 1.7.0 → 1.7.2
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/dist/{chunk-E63GTCOE.js → chunk-KPDHH2AZ.js} +623 -244
- package/dist/chunk-KPDHH2AZ.js.map +1 -0
- package/dist/cli.js +34 -15
- package/dist/cli.js.map +1 -1
- package/dist/main.js +1 -1
- package/package.json +1 -1
- package/skills/best-practices/SKILL.md +12 -7
- package/dist/chunk-E63GTCOE.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
|
}
|
|
@@ -265,7 +270,9 @@ function detectProjectContext(targetPath) {
|
|
|
265
270
|
if (skill.dependencies.length === 0) {
|
|
266
271
|
continue;
|
|
267
272
|
}
|
|
268
|
-
const hasMatch = skill.dependencies.some(
|
|
273
|
+
const hasMatch = skill.dependencies.some(
|
|
274
|
+
(dependencyName) => dependencies.includes(dependencyName)
|
|
275
|
+
);
|
|
269
276
|
if (hasMatch && !suggestedSkillIds.includes(skill.id)) {
|
|
270
277
|
suggestedSkillIds.push(skill.id);
|
|
271
278
|
}
|
|
@@ -284,21 +291,21 @@ function detectProjectContext(targetPath) {
|
|
|
284
291
|
}
|
|
285
292
|
|
|
286
293
|
// src/domains/ui/ui-orchestrator.ts
|
|
287
|
-
import * as
|
|
288
|
-
import
|
|
294
|
+
import * as prompts6 from "@clack/prompts";
|
|
295
|
+
import picocolors4 from "picocolors";
|
|
289
296
|
|
|
290
297
|
// src/domains/ui/banners.ts
|
|
291
|
-
import
|
|
298
|
+
import picocolors from "picocolors";
|
|
292
299
|
function renderBanner() {
|
|
293
|
-
const title =
|
|
294
|
-
const badge =
|
|
295
|
-
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");
|
|
296
303
|
return `${badge} ${subtitle}
|
|
297
304
|
`;
|
|
298
305
|
}
|
|
299
306
|
|
|
300
307
|
// src/domains/ui/prompt-skills.ts
|
|
301
|
-
import * as
|
|
308
|
+
import * as prompts from "@clack/prompts";
|
|
302
309
|
async function promptSkills(suggestedSkillIds) {
|
|
303
310
|
const selectableSkills = SKILL_CATALOG.filter((skill) => !skill.isCoreHarness);
|
|
304
311
|
const options = selectableSkills.map((skill) => {
|
|
@@ -309,7 +316,7 @@ async function promptSkills(suggestedSkillIds) {
|
|
|
309
316
|
hint: isSuggested ? "Suggested based on your project stack" : skill.description
|
|
310
317
|
};
|
|
311
318
|
});
|
|
312
|
-
const selected = await
|
|
319
|
+
const selected = await prompts.multiselect({
|
|
313
320
|
message: "Which skills would you like to install in this project?",
|
|
314
321
|
options,
|
|
315
322
|
initialValues: suggestedSkillIds,
|
|
@@ -319,7 +326,7 @@ async function promptSkills(suggestedSkillIds) {
|
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
// src/domains/ui/prompt-agents.ts
|
|
322
|
-
import * as
|
|
329
|
+
import * as prompts2 from "@clack/prompts";
|
|
323
330
|
async function promptAgents(detectedAgents) {
|
|
324
331
|
const options = [
|
|
325
332
|
{ value: "antigravity", label: "Antigravity", hint: ".agents/skills (Google Agentic IDE)" },
|
|
@@ -328,7 +335,7 @@ async function promptAgents(detectedAgents) {
|
|
|
328
335
|
{ value: "gemini", label: "Gemini Code Assist", hint: ".gemini/skills (Google Gemini)" }
|
|
329
336
|
];
|
|
330
337
|
const initialValues = detectedAgents.length > 0 ? detectedAgents : ["antigravity"];
|
|
331
|
-
const selected = await
|
|
338
|
+
const selected = await prompts2.multiselect({
|
|
332
339
|
message: "Which AI agents should be configured?",
|
|
333
340
|
options,
|
|
334
341
|
initialValues,
|
|
@@ -338,16 +345,16 @@ async function promptAgents(detectedAgents) {
|
|
|
338
345
|
}
|
|
339
346
|
|
|
340
347
|
// src/domains/ui/prompt-language.ts
|
|
341
|
-
import * as
|
|
348
|
+
import * as prompts3 from "@clack/prompts";
|
|
342
349
|
async function promptLanguage(defaultLanguage = "es") {
|
|
343
350
|
const options = [
|
|
344
|
-
{ value: "es", label: "Espa\xF1ol (Spanish)", hint: "
|
|
351
|
+
{ value: "es", label: "Espa\xF1ol (Spanish)", hint: "Recommended for Spanish-speaking projects" },
|
|
345
352
|
{ value: "en", label: "English", hint: "Standard international English" },
|
|
346
|
-
{ value: "pt", label: "Portugu\xEAs (Portuguese)", hint: "
|
|
347
|
-
{ value: "it", label: "Italiano (Italian)", hint: "
|
|
348
|
-
{ 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" }
|
|
349
356
|
];
|
|
350
|
-
const selected = await
|
|
357
|
+
const selected = await prompts3.select({
|
|
351
358
|
message: "Which language should AI agents use for user-facing copy and communication?",
|
|
352
359
|
options,
|
|
353
360
|
initialValue: defaultLanguage
|
|
@@ -356,9 +363,9 @@ async function promptLanguage(defaultLanguage = "es") {
|
|
|
356
363
|
}
|
|
357
364
|
|
|
358
365
|
// src/domains/ui/prompt-confirm-overwrite.ts
|
|
359
|
-
import * as
|
|
366
|
+
import * as prompts4 from "@clack/prompts";
|
|
360
367
|
async function promptConfirmOverwrite() {
|
|
361
|
-
return await
|
|
368
|
+
return await prompts4.confirm({
|
|
362
369
|
message: "Some skills already exist in this project. Would you like to overwrite them with the latest version?",
|
|
363
370
|
initialValue: true
|
|
364
371
|
});
|
|
@@ -368,10 +375,10 @@ async function promptConfirmOverwrite() {
|
|
|
368
375
|
import { lstatSync } from "fs";
|
|
369
376
|
import { join as join3 } from "path";
|
|
370
377
|
var AGENT_DESTINATION_MAP = {
|
|
371
|
-
antigravity: (
|
|
372
|
-
claude: (
|
|
373
|
-
cursor: (
|
|
374
|
-
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)
|
|
375
382
|
};
|
|
376
383
|
function verifySkillExists(targetPath, skillDirectoryName, agents) {
|
|
377
384
|
for (const agent of agents) {
|
|
@@ -387,10 +394,10 @@ function verifySkillExists(targetPath, skillDirectoryName, agents) {
|
|
|
387
394
|
}
|
|
388
395
|
|
|
389
396
|
// src/domains/installer/install-orchestrator.ts
|
|
390
|
-
import { join as
|
|
397
|
+
import { join as join12 } from "path";
|
|
391
398
|
|
|
392
399
|
// src/domains/adapters/skill-reference-builder.ts
|
|
393
|
-
import { existsSync as
|
|
400
|
+
import { existsSync as existsSync5, lstatSync as lstatSync3, mkdirSync, readFileSync as readFileSync3, rmSync as rmSync2, unlinkSync } from "fs";
|
|
394
401
|
import { join as join5 } from "path";
|
|
395
402
|
|
|
396
403
|
// src/domains/project-store/project-paths.ts
|
|
@@ -412,48 +419,194 @@ function getProjectConfigPath(targetPath) {
|
|
|
412
419
|
}
|
|
413
420
|
|
|
414
421
|
// src/domains/fs/safe-file-writer.ts
|
|
415
|
-
import { chmodSync, existsSync as
|
|
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
|
+
}
|
|
416
521
|
|
|
417
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
|
+
}
|
|
418
547
|
var FilePermissionError = class extends Error {
|
|
419
548
|
code = "EACCES";
|
|
420
549
|
filePath;
|
|
550
|
+
diagnosis;
|
|
421
551
|
suggestedFixCommand;
|
|
422
|
-
constructor(filePath) {
|
|
423
|
-
|
|
552
|
+
constructor(filePath, diagnosis) {
|
|
553
|
+
const resolved = diagnosis ?? inspectFileAccess(filePath);
|
|
554
|
+
const reason = buildDiagnosisReason(resolved);
|
|
555
|
+
super(`Cannot write to ${filePath} (${reason})`);
|
|
424
556
|
this.name = "FilePermissionError";
|
|
425
557
|
this.filePath = filePath;
|
|
426
|
-
this.
|
|
558
|
+
this.diagnosis = resolved;
|
|
559
|
+
this.suggestedFixCommand = buildSuggestedFix(resolved);
|
|
427
560
|
}
|
|
428
561
|
};
|
|
429
562
|
|
|
430
563
|
// src/domains/fs/safe-file-writer.ts
|
|
431
|
-
function safeWriteFileSync(filePath, content) {
|
|
564
|
+
function safeWriteFileSync(filePath, content, options) {
|
|
565
|
+
const chmodFunction = options?.chmodImpl ?? chmodSync;
|
|
432
566
|
try {
|
|
433
567
|
writeFileSync(filePath, content, "utf-8");
|
|
568
|
+
return { strategy: "direct", previousOwnerUid: null };
|
|
434
569
|
} catch (error) {
|
|
435
|
-
const
|
|
436
|
-
if (
|
|
437
|
-
|
|
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)) {
|
|
438
594
|
try {
|
|
439
|
-
|
|
440
|
-
writeFileSync(filePath, content, "utf-8");
|
|
441
|
-
return;
|
|
595
|
+
rmSync(tempPath, { force: true });
|
|
442
596
|
} catch {
|
|
443
597
|
}
|
|
444
598
|
}
|
|
445
|
-
throw new FilePermissionError(filePath);
|
|
446
599
|
}
|
|
447
|
-
throw error;
|
|
448
600
|
}
|
|
601
|
+
throw new FilePermissionError(filePath, diagnosis);
|
|
449
602
|
}
|
|
450
603
|
|
|
451
604
|
// src/domains/adapters/skill-reference-builder.ts
|
|
452
605
|
function removeExistingPathSafely(path) {
|
|
453
|
-
const fileExists =
|
|
606
|
+
const fileExists = existsSync5(path);
|
|
454
607
|
if (!fileExists) {
|
|
455
608
|
try {
|
|
456
|
-
const lstat =
|
|
609
|
+
const lstat = lstatSync3(path);
|
|
457
610
|
if (lstat.isSymbolicLink()) {
|
|
458
611
|
unlinkSync(path);
|
|
459
612
|
}
|
|
@@ -462,13 +615,13 @@ function removeExistingPathSafely(path) {
|
|
|
462
615
|
return;
|
|
463
616
|
}
|
|
464
617
|
try {
|
|
465
|
-
const lstat =
|
|
618
|
+
const lstat = lstatSync3(path);
|
|
466
619
|
if (lstat.isSymbolicLink()) {
|
|
467
620
|
unlinkSync(path);
|
|
468
621
|
return;
|
|
469
622
|
}
|
|
470
623
|
if (lstat.isDirectory()) {
|
|
471
|
-
|
|
624
|
+
rmSync2(path, { recursive: true, force: true });
|
|
472
625
|
return;
|
|
473
626
|
}
|
|
474
627
|
unlinkSync(path);
|
|
@@ -476,7 +629,7 @@ function removeExistingPathSafely(path) {
|
|
|
476
629
|
}
|
|
477
630
|
}
|
|
478
631
|
function extractSkillDescription(canonicalSkillMdPath, fallback) {
|
|
479
|
-
if (!
|
|
632
|
+
if (!existsSync5(canonicalSkillMdPath)) {
|
|
480
633
|
return fallback;
|
|
481
634
|
}
|
|
482
635
|
try {
|
|
@@ -526,7 +679,7 @@ function writeCursorRuleReference(targetPath, cursorRelativeRulesDir, skill, tra
|
|
|
526
679
|
transaction.recordCreatedDir(rulesDir);
|
|
527
680
|
transaction.recordBeforeWrite(targetFile);
|
|
528
681
|
}
|
|
529
|
-
if (!
|
|
682
|
+
if (!existsSync5(rulesDir)) {
|
|
530
683
|
mkdirSync(rulesDir, { recursive: true });
|
|
531
684
|
}
|
|
532
685
|
removeExistingPathSafely(targetFile);
|
|
@@ -596,31 +749,31 @@ var GeminiAdapter = class {
|
|
|
596
749
|
};
|
|
597
750
|
|
|
598
751
|
// src/domains/project-store/sync-project-store.ts
|
|
599
|
-
import { cpSync, existsSync as
|
|
752
|
+
import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync2, rmSync as rmSync3 } from "fs";
|
|
600
753
|
import { join as join7 } from "path";
|
|
601
754
|
|
|
602
755
|
// src/domains/installer/resolve-skills-source.ts
|
|
603
756
|
import { fileURLToPath } from "url";
|
|
604
|
-
import { dirname, resolve, join as join6 } from "path";
|
|
605
|
-
import { existsSync as
|
|
757
|
+
import { dirname as dirname2, resolve, join as join6 } from "path";
|
|
758
|
+
import { existsSync as existsSync6 } from "fs";
|
|
606
759
|
function resolveSkillsSourceRoot() {
|
|
607
760
|
const currentFilePath = fileURLToPath(import.meta.url);
|
|
608
|
-
const currentDir =
|
|
761
|
+
const currentDir = dirname2(currentFilePath);
|
|
609
762
|
const candidateFromDist = resolve(currentDir, "../skills");
|
|
610
|
-
if (
|
|
763
|
+
if (existsSync6(candidateFromDist)) {
|
|
611
764
|
return candidateFromDist;
|
|
612
765
|
}
|
|
613
766
|
const candidateFromSrc = resolve(currentDir, "../../../skills");
|
|
614
|
-
if (
|
|
767
|
+
if (existsSync6(candidateFromSrc)) {
|
|
615
768
|
return candidateFromSrc;
|
|
616
769
|
}
|
|
617
770
|
let searchDir = currentDir;
|
|
618
|
-
for (let
|
|
771
|
+
for (let depth = 0; depth < 5; depth++) {
|
|
619
772
|
const candidate = join6(searchDir, "skills");
|
|
620
|
-
if (
|
|
773
|
+
if (existsSync6(candidate)) {
|
|
621
774
|
return candidate;
|
|
622
775
|
}
|
|
623
|
-
const parent =
|
|
776
|
+
const parent = dirname2(searchDir);
|
|
624
777
|
if (parent === searchDir) {
|
|
625
778
|
break;
|
|
626
779
|
}
|
|
@@ -638,13 +791,13 @@ function syncProjectStore(targetPath, selectedSkillIds, _selectedAgents, transac
|
|
|
638
791
|
if (transaction) {
|
|
639
792
|
transaction.recordCreatedDir(projectSkillsDir);
|
|
640
793
|
}
|
|
641
|
-
if (!
|
|
794
|
+
if (!existsSync7(projectSkillsDir)) {
|
|
642
795
|
mkdirSync2(projectSkillsDir, { recursive: true });
|
|
643
796
|
}
|
|
644
797
|
const legacyCursorRules = join7(targetPath, ".grooveskills", "cursor-rules");
|
|
645
|
-
if (
|
|
798
|
+
if (existsSync7(legacyCursorRules)) {
|
|
646
799
|
try {
|
|
647
|
-
|
|
800
|
+
rmSync3(legacyCursorRules, { recursive: true, force: true });
|
|
648
801
|
} catch {
|
|
649
802
|
}
|
|
650
803
|
}
|
|
@@ -658,14 +811,14 @@ function syncProjectStore(targetPath, selectedSkillIds, _selectedAgents, transac
|
|
|
658
811
|
if (transaction) {
|
|
659
812
|
transaction.recordCreatedDir(destSkillDir);
|
|
660
813
|
}
|
|
661
|
-
if (
|
|
814
|
+
if (existsSync7(sourceSkillDir)) {
|
|
662
815
|
cpSync(sourceSkillDir, destSkillDir, { recursive: true });
|
|
663
816
|
}
|
|
664
817
|
}
|
|
665
818
|
}
|
|
666
819
|
|
|
667
820
|
// src/domains/harness/install-harness.ts
|
|
668
|
-
import { existsSync as
|
|
821
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "fs";
|
|
669
822
|
import { join as join10 } from "path";
|
|
670
823
|
|
|
671
824
|
// src/domains/harness/harness-languages.ts
|
|
@@ -714,14 +867,14 @@ var SUPPORTED_LANGUAGES = {
|
|
|
714
867
|
|
|
715
868
|
// src/domains/harness/harness-templates.ts
|
|
716
869
|
function buildContextTemplate(detectedDependencies, language = "es", profile) {
|
|
717
|
-
const
|
|
718
|
-
const
|
|
870
|
+
const dependenciesFormatted = detectedDependencies.length > 0 ? detectedDependencies.map((dependencyName) => `- \`${dependencyName}\``).join("\n") : "- (No external dependencies detected)";
|
|
871
|
+
const languageConfig = SUPPORTED_LANGUAGES[language];
|
|
719
872
|
const projectName = profile?.projectName ?? "Project";
|
|
720
|
-
const
|
|
873
|
+
const packageManager = profile?.packageManager ?? "npm";
|
|
721
874
|
const commandLines = [];
|
|
722
875
|
if (profile?.commands) {
|
|
723
|
-
for (const [
|
|
724
|
-
commandLines.push(`- **${
|
|
876
|
+
for (const [commandKey, commandValue] of Object.entries(profile.commands)) {
|
|
877
|
+
commandLines.push(`- **${commandKey}**: \`${commandValue}\``);
|
|
725
878
|
}
|
|
726
879
|
}
|
|
727
880
|
const commandsSection = commandLines.length > 0 ? commandLines.join("\n") : `- Build: Refer to \`package.json\` scripts.
|
|
@@ -729,62 +882,62 @@ function buildContextTemplate(detectedDependencies, language = "es", profile) {
|
|
|
729
882
|
- Tests: Refer to test runner configuration.`;
|
|
730
883
|
const aliasLines = [];
|
|
731
884
|
if (profile?.pathAliases) {
|
|
732
|
-
for (const [
|
|
733
|
-
aliasLines.push(`- \`${
|
|
885
|
+
for (const [aliasPattern, aliasTarget] of Object.entries(profile.pathAliases)) {
|
|
886
|
+
aliasLines.push(`- \`${aliasPattern}\` -> \`${aliasTarget}\``);
|
|
734
887
|
}
|
|
735
888
|
}
|
|
736
889
|
const aliasesSection = aliasLines.length > 0 ? `
|
|
737
890
|
### Path Aliases
|
|
738
891
|
${aliasLines.join("\n")}
|
|
739
892
|
` : "";
|
|
740
|
-
const
|
|
893
|
+
const directoryLines = [];
|
|
741
894
|
if (profile?.sourceDirectories && profile.sourceDirectories.length > 0) {
|
|
742
|
-
for (const
|
|
743
|
-
|
|
895
|
+
for (const directoryName of profile.sourceDirectories) {
|
|
896
|
+
directoryLines.push(`- \`${directoryName}/\``);
|
|
744
897
|
}
|
|
745
898
|
}
|
|
746
|
-
const
|
|
899
|
+
const directoriesSection = directoryLines.length > 0 ? `
|
|
747
900
|
### Source Modules
|
|
748
|
-
${
|
|
901
|
+
${directoryLines.join("\n")}
|
|
749
902
|
` : "";
|
|
750
903
|
return `# Project Context & Architecture: ${projectName}
|
|
751
904
|
|
|
752
905
|
This file provides AI agents with instant architectural orientation for this project.
|
|
753
906
|
|
|
754
907
|
## 1. Stack & Dependencies
|
|
755
|
-
- **Package Manager**: \`${
|
|
756
|
-
${
|
|
908
|
+
- **Package Manager**: \`${packageManager}\`
|
|
909
|
+
${dependenciesFormatted}
|
|
757
910
|
|
|
758
911
|
## 2. Directory Structure Conventions
|
|
759
912
|
- Follow single-responsibility per file (<200 lines).
|
|
760
913
|
- Maintain direct imports with path aliases; avoid barrel files.
|
|
761
914
|
- Keep domain boundaries explicit and clean.
|
|
762
|
-
${aliasesSection}${
|
|
915
|
+
${aliasesSection}${directoriesSection}
|
|
763
916
|
## 3. Communication & User Copy Language
|
|
764
|
-
- **Configured Language**: ${
|
|
765
|
-
- **Policy**: ${
|
|
917
|
+
- **Configured Language**: ${languageConfig.nativeName} (\`${language}\`).
|
|
918
|
+
- **Policy**: ${languageConfig.instruction}
|
|
766
919
|
|
|
767
920
|
## 4. Essential Commands
|
|
768
921
|
${commandsSection}
|
|
769
922
|
`;
|
|
770
923
|
}
|
|
771
924
|
function buildDecisionsTemplate(language = "es") {
|
|
772
|
-
const
|
|
773
|
-
const
|
|
925
|
+
const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
926
|
+
const languageConfig = SUPPORTED_LANGUAGES[language];
|
|
774
927
|
return `# Architecture Decision Records (ADR)
|
|
775
928
|
|
|
776
929
|
An append-only log of significant technical and architectural choices.
|
|
777
930
|
|
|
778
931
|
| Date | Title | Decision | Reason & Consequences |
|
|
779
932
|
| :--- | :--- | :--- | :--- |
|
|
780
|
-
| ${
|
|
933
|
+
| ${currentDate} | Adopt GrooveSkills Harness | Initialized \`.grooveskills/\` control plane with ODD | Clean, encapsulated agent memory, 3-lane ODD flow, and ${languageConfig.nativeName} copy standard |
|
|
781
934
|
`;
|
|
782
935
|
}
|
|
783
936
|
function buildOnboardingTaskTemplate(projectName = "Project") {
|
|
784
|
-
const
|
|
937
|
+
const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
785
938
|
return `---
|
|
786
939
|
status: pending
|
|
787
|
-
createdAt: ${
|
|
940
|
+
createdAt: ${currentDate}
|
|
788
941
|
---
|
|
789
942
|
|
|
790
943
|
# 00 \u2014 Initial Project Onboarding & Architecture Exploration: ${projectName}
|
|
@@ -840,7 +993,7 @@ By [Output / Technical Change], [Outcome / Verifiable Behavior] will occur, prov
|
|
|
840
993
|
`;
|
|
841
994
|
}
|
|
842
995
|
function buildAgentHarnessBlock(language = "es") {
|
|
843
|
-
const
|
|
996
|
+
const languageConfig = SUPPORTED_LANGUAGES[language];
|
|
844
997
|
return `<!-- grooveskills:harness:start -->
|
|
845
998
|
# GrooveSkills Harness \u2014 Agent Rules & Memory
|
|
846
999
|
|
|
@@ -853,9 +1006,9 @@ This project uses the **GrooveSkills Harness** located in \`.grooveskills/\`.
|
|
|
853
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\`.
|
|
854
1007
|
- **Lane 3 (Spec)**: Ambiguous or high-impact architecture. Clarify and validate the spec in \`.grooveskills/memory/tasks/<slug>.md\` before writing code.
|
|
855
1008
|
|
|
856
|
-
## 2. User Communication Language (${
|
|
857
|
-
- **Language**: \`${language}\` (${
|
|
858
|
-
- **Requirement**: ${
|
|
1009
|
+
## 2. User Communication Language (${languageConfig.nativeName})
|
|
1010
|
+
- **Language**: \`${language}\` (${languageConfig.nativeName}).
|
|
1011
|
+
- **Requirement**: ${languageConfig.instruction}
|
|
859
1012
|
|
|
860
1013
|
## 3. Memory Locations
|
|
861
1014
|
- Architecture context: \`.grooveskills/memory/context.md\`
|
|
@@ -865,11 +1018,11 @@ This project uses the **GrooveSkills Harness** located in \`.grooveskills/\`.
|
|
|
865
1018
|
}
|
|
866
1019
|
|
|
867
1020
|
// src/domains/harness/scan-project-onboarding.ts
|
|
868
|
-
import { existsSync as
|
|
1021
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
869
1022
|
import { basename, join as join8 } from "path";
|
|
870
1023
|
function detectProjectName(targetPath) {
|
|
871
1024
|
const packageJsonPath = join8(targetPath, "package.json");
|
|
872
|
-
if (
|
|
1025
|
+
if (existsSync8(packageJsonPath)) {
|
|
873
1026
|
try {
|
|
874
1027
|
const raw = readFileSync4(packageJsonPath, "utf-8");
|
|
875
1028
|
const parsed = JSON.parse(raw);
|
|
@@ -880,7 +1033,7 @@ function detectProjectName(targetPath) {
|
|
|
880
1033
|
}
|
|
881
1034
|
}
|
|
882
1035
|
const cargoPath = join8(targetPath, "Cargo.toml");
|
|
883
|
-
if (
|
|
1036
|
+
if (existsSync8(cargoPath)) {
|
|
884
1037
|
try {
|
|
885
1038
|
const cargoRaw = readFileSync4(cargoPath, "utf-8");
|
|
886
1039
|
const nameMatch = cargoRaw.match(/name\s*=\s*"([^"]+)"/);
|
|
@@ -891,12 +1044,12 @@ function detectProjectName(targetPath) {
|
|
|
891
1044
|
}
|
|
892
1045
|
}
|
|
893
1046
|
const pyprojectPath = join8(targetPath, "pyproject.toml");
|
|
894
|
-
if (
|
|
1047
|
+
if (existsSync8(pyprojectPath)) {
|
|
895
1048
|
try {
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
if (
|
|
899
|
-
return
|
|
1049
|
+
const pyprojectRaw = readFileSync4(pyprojectPath, "utf-8");
|
|
1050
|
+
const nameMatch = pyprojectRaw.match(/name\s*=\s*"([^"]+)"/);
|
|
1051
|
+
if (nameMatch?.[1]) {
|
|
1052
|
+
return nameMatch[1];
|
|
900
1053
|
}
|
|
901
1054
|
} catch {
|
|
902
1055
|
}
|
|
@@ -904,32 +1057,32 @@ function detectProjectName(targetPath) {
|
|
|
904
1057
|
return basename(targetPath) || "project";
|
|
905
1058
|
}
|
|
906
1059
|
function detectPackageManager(targetPath) {
|
|
907
|
-
if (
|
|
1060
|
+
if (existsSync8(join8(targetPath, "pnpm-lock.yaml"))) {
|
|
908
1061
|
return "pnpm";
|
|
909
1062
|
}
|
|
910
|
-
if (
|
|
1063
|
+
if (existsSync8(join8(targetPath, "bun.lockb")) || existsSync8(join8(targetPath, "bun.lock"))) {
|
|
911
1064
|
return "bun";
|
|
912
1065
|
}
|
|
913
|
-
if (
|
|
1066
|
+
if (existsSync8(join8(targetPath, "yarn.lock"))) {
|
|
914
1067
|
return "yarn";
|
|
915
1068
|
}
|
|
916
|
-
if (
|
|
1069
|
+
if (existsSync8(join8(targetPath, "package-lock.json"))) {
|
|
917
1070
|
return "npm";
|
|
918
1071
|
}
|
|
919
|
-
if (
|
|
1072
|
+
if (existsSync8(join8(targetPath, "Cargo.lock")) || existsSync8(join8(targetPath, "Cargo.toml"))) {
|
|
920
1073
|
return "cargo";
|
|
921
1074
|
}
|
|
922
|
-
if (
|
|
1075
|
+
if (existsSync8(join8(targetPath, "uv.lock"))) {
|
|
923
1076
|
return "uv";
|
|
924
1077
|
}
|
|
925
|
-
if (
|
|
1078
|
+
if (existsSync8(join8(targetPath, "poetry.lock"))) {
|
|
926
1079
|
return "poetry";
|
|
927
1080
|
}
|
|
928
1081
|
return "npm";
|
|
929
1082
|
}
|
|
930
|
-
function detectCommands(targetPath,
|
|
1083
|
+
function detectCommands(targetPath, packageManager) {
|
|
931
1084
|
const packageJsonPath = join8(targetPath, "package.json");
|
|
932
|
-
if (!
|
|
1085
|
+
if (!existsSync8(packageJsonPath)) {
|
|
933
1086
|
return {};
|
|
934
1087
|
}
|
|
935
1088
|
try {
|
|
@@ -937,7 +1090,7 @@ function detectCommands(targetPath, pkgManager) {
|
|
|
937
1090
|
const parsed = JSON.parse(raw);
|
|
938
1091
|
const scripts = parsed.scripts ?? {};
|
|
939
1092
|
const commands = {};
|
|
940
|
-
const runner =
|
|
1093
|
+
const runner = packageManager === "npm" ? "npm run" : packageManager;
|
|
941
1094
|
if (scripts.build) {
|
|
942
1095
|
commands.build = `${runner} build`;
|
|
943
1096
|
}
|
|
@@ -964,7 +1117,7 @@ function detectCommands(targetPath, pkgManager) {
|
|
|
964
1117
|
}
|
|
965
1118
|
function detectPathAliases(targetPath) {
|
|
966
1119
|
const tsconfigPath = join8(targetPath, "tsconfig.json");
|
|
967
|
-
if (!
|
|
1120
|
+
if (!existsSync8(tsconfigPath)) {
|
|
968
1121
|
return {};
|
|
969
1122
|
}
|
|
970
1123
|
try {
|
|
@@ -972,9 +1125,9 @@ function detectPathAliases(targetPath) {
|
|
|
972
1125
|
const parsed = JSON.parse(raw);
|
|
973
1126
|
const paths = parsed.compilerOptions?.paths ?? {};
|
|
974
1127
|
const aliases = {};
|
|
975
|
-
for (const [
|
|
976
|
-
if (Array.isArray(
|
|
977
|
-
aliases[
|
|
1128
|
+
for (const [aliasKey, aliasTargets] of Object.entries(paths)) {
|
|
1129
|
+
if (Array.isArray(aliasTargets) && typeof aliasTargets[0] === "string") {
|
|
1130
|
+
aliases[aliasKey] = aliasTargets[0];
|
|
978
1131
|
}
|
|
979
1132
|
}
|
|
980
1133
|
return aliases;
|
|
@@ -984,7 +1137,7 @@ function detectPathAliases(targetPath) {
|
|
|
984
1137
|
}
|
|
985
1138
|
function detectSourceDirectories(targetPath) {
|
|
986
1139
|
const srcPath = join8(targetPath, "src");
|
|
987
|
-
const scanTarget =
|
|
1140
|
+
const scanTarget = existsSync8(srcPath) ? srcPath : targetPath;
|
|
988
1141
|
try {
|
|
989
1142
|
const entries = readdirSync2(scanTarget, { withFileTypes: true });
|
|
990
1143
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => !name.startsWith(".") && name !== "node_modules" && name !== "dist");
|
|
@@ -1008,30 +1161,36 @@ function scanProjectOnboarding(targetPath) {
|
|
|
1008
1161
|
}
|
|
1009
1162
|
|
|
1010
1163
|
// src/domains/harness/upsert-agent-instructions.ts
|
|
1011
|
-
import { existsSync as
|
|
1164
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
1012
1165
|
import { join as join9 } from "path";
|
|
1013
1166
|
var START_MARKER = "<!-- grooveskills:harness:start -->";
|
|
1014
1167
|
var END_MARKER = "<!-- grooveskills:harness:end -->";
|
|
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
|
+
}
|
|
1015
1174
|
function applyHarnessBlock(filePath, language = "es", transaction) {
|
|
1016
1175
|
if (transaction) {
|
|
1017
1176
|
transaction.recordBeforeWrite(filePath);
|
|
1018
1177
|
}
|
|
1019
1178
|
const block = buildAgentHarnessBlock(language);
|
|
1020
|
-
if (!
|
|
1021
|
-
|
|
1022
|
-
|
|
1179
|
+
if (!existsSync9(filePath)) {
|
|
1180
|
+
writeWithTransaction(filePath, `${block}
|
|
1181
|
+
`, transaction);
|
|
1023
1182
|
return;
|
|
1024
1183
|
}
|
|
1025
1184
|
const existingContent = readFileSync5(filePath, "utf-8");
|
|
1026
1185
|
if (existingContent.includes(START_MARKER) && existingContent.includes(END_MARKER)) {
|
|
1027
1186
|
const regex = new RegExp(`${START_MARKER}[\\s\\S]*?${END_MARKER}`, "g");
|
|
1028
1187
|
const updated = existingContent.replace(regex, block);
|
|
1029
|
-
|
|
1188
|
+
writeWithTransaction(filePath, updated, transaction);
|
|
1030
1189
|
return;
|
|
1031
1190
|
}
|
|
1032
1191
|
const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
|
|
1033
|
-
|
|
1034
|
-
|
|
1192
|
+
writeWithTransaction(filePath, `${existingContent}${separator}${block}
|
|
1193
|
+
`, transaction);
|
|
1035
1194
|
}
|
|
1036
1195
|
function upsertAgentInstructions(targetPath, selectedAgents, language = "es", transaction) {
|
|
1037
1196
|
const updatedFiles = [];
|
|
@@ -1047,65 +1206,63 @@ function upsertAgentInstructions(targetPath, selectedAgents, language = "es", tr
|
|
|
1047
1206
|
}
|
|
1048
1207
|
|
|
1049
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
|
+
}
|
|
1050
1218
|
function installHarness(options) {
|
|
1051
1219
|
const memoryDir = getProjectMemoryDir(options.targetPath);
|
|
1052
1220
|
const tasksDir = getProjectTasksDir(options.targetPath);
|
|
1053
1221
|
const language = options.language ?? "es";
|
|
1054
1222
|
const forceOnboarding = Boolean(options.forceOnboarding);
|
|
1055
|
-
const
|
|
1056
|
-
if (
|
|
1057
|
-
|
|
1058
|
-
|
|
1223
|
+
const transaction = options.transaction;
|
|
1224
|
+
if (transaction) {
|
|
1225
|
+
transaction.recordCreatedDir(memoryDir);
|
|
1226
|
+
transaction.recordCreatedDir(tasksDir);
|
|
1059
1227
|
}
|
|
1060
|
-
if (!
|
|
1228
|
+
if (!existsSync10(tasksDir)) {
|
|
1061
1229
|
mkdirSync3(tasksDir, { recursive: true });
|
|
1062
1230
|
}
|
|
1063
1231
|
const createdFiles = [];
|
|
1064
1232
|
const contextPath = join10(memoryDir, "context.md");
|
|
1065
|
-
const shouldWriteContext = !
|
|
1233
|
+
const shouldWriteContext = !existsSync10(contextPath) || forceOnboarding;
|
|
1066
1234
|
if (shouldWriteContext) {
|
|
1067
|
-
if (tx) {
|
|
1068
|
-
tx.recordBeforeWrite(contextPath);
|
|
1069
|
-
}
|
|
1070
1235
|
const profile = scanProjectOnboarding(options.targetPath);
|
|
1071
1236
|
const contextContent = buildContextTemplate(
|
|
1072
1237
|
options.detectedDependencies,
|
|
1073
1238
|
language,
|
|
1074
1239
|
profile
|
|
1075
1240
|
);
|
|
1076
|
-
|
|
1241
|
+
writeTrackedFile(contextPath, contextContent, transaction);
|
|
1077
1242
|
createdFiles.push(contextPath);
|
|
1078
1243
|
const onboardingTaskPath = join10(tasksDir, "00-onboarding.md");
|
|
1079
|
-
|
|
1080
|
-
tx.recordBeforeWrite(onboardingTaskPath);
|
|
1081
|
-
}
|
|
1082
|
-
safeWriteFileSync(
|
|
1244
|
+
writeTrackedFile(
|
|
1083
1245
|
onboardingTaskPath,
|
|
1084
|
-
buildOnboardingTaskTemplate(profile.projectName)
|
|
1246
|
+
buildOnboardingTaskTemplate(profile.projectName),
|
|
1247
|
+
transaction
|
|
1085
1248
|
);
|
|
1086
1249
|
createdFiles.push(onboardingTaskPath);
|
|
1087
1250
|
}
|
|
1088
1251
|
const decisionsPath = join10(memoryDir, "decisions.md");
|
|
1089
|
-
if (!
|
|
1090
|
-
|
|
1091
|
-
tx.recordBeforeWrite(decisionsPath);
|
|
1092
|
-
}
|
|
1093
|
-
safeWriteFileSync(decisionsPath, buildDecisionsTemplate(language));
|
|
1252
|
+
if (!existsSync10(decisionsPath)) {
|
|
1253
|
+
writeTrackedFile(decisionsPath, buildDecisionsTemplate(language), transaction);
|
|
1094
1254
|
createdFiles.push(decisionsPath);
|
|
1095
1255
|
}
|
|
1096
1256
|
const templateTaskPath = join10(tasksDir, "_template.md");
|
|
1097
|
-
if (!
|
|
1098
|
-
|
|
1099
|
-
tx.recordBeforeWrite(templateTaskPath);
|
|
1100
|
-
}
|
|
1101
|
-
safeWriteFileSync(templateTaskPath, buildTaskTemplate());
|
|
1257
|
+
if (!existsSync10(templateTaskPath)) {
|
|
1258
|
+
writeTrackedFile(templateTaskPath, buildTaskTemplate(), transaction);
|
|
1102
1259
|
createdFiles.push(templateTaskPath);
|
|
1103
1260
|
}
|
|
1104
1261
|
const instructions = upsertAgentInstructions(
|
|
1105
1262
|
options.targetPath,
|
|
1106
1263
|
options.selectedAgents,
|
|
1107
1264
|
language,
|
|
1108
|
-
|
|
1265
|
+
transaction
|
|
1109
1266
|
);
|
|
1110
1267
|
createdFiles.push(...instructions);
|
|
1111
1268
|
return {
|
|
@@ -1115,14 +1272,14 @@ function installHarness(options) {
|
|
|
1115
1272
|
}
|
|
1116
1273
|
|
|
1117
1274
|
// src/domains/harness/write-project-config.ts
|
|
1118
|
-
import { existsSync as
|
|
1275
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
|
|
1119
1276
|
function readExistingConfig(configPath) {
|
|
1120
1277
|
const fallback = {
|
|
1121
1278
|
skills: [],
|
|
1122
1279
|
agents: [],
|
|
1123
1280
|
harness: false
|
|
1124
1281
|
};
|
|
1125
|
-
if (!
|
|
1282
|
+
if (!existsSync11(configPath)) {
|
|
1126
1283
|
return fallback;
|
|
1127
1284
|
}
|
|
1128
1285
|
try {
|
|
@@ -1156,21 +1313,25 @@ function writeProjectConfig(targetPath, newSkills, newAgents, harnessEnabled, la
|
|
|
1156
1313
|
skills: mergedSkills,
|
|
1157
1314
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1158
1315
|
};
|
|
1159
|
-
safeWriteFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1316
|
+
const outcome = safeWriteFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1160
1317
|
`);
|
|
1318
|
+
if (outcome.strategy === "replaced" && transaction) {
|
|
1319
|
+
transaction.recordReplacedFile(configPath, outcome.previousOwnerUid);
|
|
1320
|
+
}
|
|
1161
1321
|
return configPath;
|
|
1162
1322
|
}
|
|
1163
1323
|
|
|
1164
1324
|
// src/domains/fs/install-transaction.ts
|
|
1165
|
-
import { existsSync as
|
|
1325
|
+
import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
|
|
1166
1326
|
var InstallTransaction = class {
|
|
1167
1327
|
originalFiles = /* @__PURE__ */ new Map();
|
|
1168
1328
|
createdDirs = [];
|
|
1329
|
+
replacedFiles = [];
|
|
1169
1330
|
recordBeforeWrite(filePath) {
|
|
1170
1331
|
if (this.originalFiles.has(filePath)) {
|
|
1171
1332
|
return;
|
|
1172
1333
|
}
|
|
1173
|
-
if (
|
|
1334
|
+
if (existsSync12(filePath)) {
|
|
1174
1335
|
try {
|
|
1175
1336
|
this.originalFiles.set(filePath, readFileSync7(filePath, "utf-8"));
|
|
1176
1337
|
} catch {
|
|
@@ -1181,42 +1342,118 @@ var InstallTransaction = class {
|
|
|
1181
1342
|
}
|
|
1182
1343
|
}
|
|
1183
1344
|
recordCreatedDir(dirPath) {
|
|
1184
|
-
if (!
|
|
1345
|
+
if (!existsSync12(dirPath) && !this.createdDirs.includes(dirPath)) {
|
|
1185
1346
|
this.createdDirs.push(dirPath);
|
|
1186
1347
|
}
|
|
1187
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
|
+
}
|
|
1188
1357
|
rollback() {
|
|
1189
1358
|
const restoredFiles = [];
|
|
1190
1359
|
const removedFiles = [];
|
|
1191
1360
|
const removedDirs = [];
|
|
1361
|
+
const failures = [];
|
|
1192
1362
|
for (const [filePath, content] of this.originalFiles.entries()) {
|
|
1193
1363
|
try {
|
|
1194
1364
|
if (content === null) {
|
|
1195
|
-
if (
|
|
1196
|
-
|
|
1365
|
+
if (existsSync12(filePath)) {
|
|
1366
|
+
rmSync4(filePath, { force: true });
|
|
1197
1367
|
removedFiles.push(filePath);
|
|
1198
1368
|
}
|
|
1199
|
-
|
|
1200
|
-
safeWriteFileSync(filePath, content);
|
|
1201
|
-
restoredFiles.push(filePath);
|
|
1369
|
+
continue;
|
|
1202
1370
|
}
|
|
1203
|
-
|
|
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 });
|
|
1204
1385
|
}
|
|
1205
1386
|
}
|
|
1206
|
-
const sortedDirs = [...this.createdDirs].sort(
|
|
1207
|
-
|
|
1387
|
+
const sortedDirs = [...this.createdDirs].sort(
|
|
1388
|
+
(firstDir, secondDir) => secondDir.length - firstDir.length
|
|
1389
|
+
);
|
|
1390
|
+
for (const directoryPath of sortedDirs) {
|
|
1208
1391
|
try {
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
1211
|
-
removedDirs.push(
|
|
1392
|
+
if (existsSync12(directoryPath) && readdirSync3(directoryPath).length === 0) {
|
|
1393
|
+
rmSync4(directoryPath, { recursive: true, force: true });
|
|
1394
|
+
removedDirs.push(directoryPath);
|
|
1212
1395
|
}
|
|
1213
|
-
} catch {
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1398
|
+
failures.push({ path: directoryPath, reason });
|
|
1214
1399
|
}
|
|
1215
1400
|
}
|
|
1216
|
-
return {
|
|
1401
|
+
return {
|
|
1402
|
+
restoredFiles,
|
|
1403
|
+
removedFiles,
|
|
1404
|
+
removedDirs,
|
|
1405
|
+
failures,
|
|
1406
|
+
isClean: failures.length === 0
|
|
1407
|
+
};
|
|
1217
1408
|
}
|
|
1218
1409
|
};
|
|
1219
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
|
+
|
|
1220
1457
|
// src/errors/install-transaction-error.ts
|
|
1221
1458
|
var InstallTransactionError = class extends Error {
|
|
1222
1459
|
stage;
|
|
@@ -1225,7 +1462,7 @@ var InstallTransactionError = class extends Error {
|
|
|
1225
1462
|
suggestedFix;
|
|
1226
1463
|
constructor(options) {
|
|
1227
1464
|
const rawMessage = options.cause instanceof Error ? options.cause.message : String(options.cause);
|
|
1228
|
-
super(`Error
|
|
1465
|
+
super(`Error during stage '${options.stage}': ${rawMessage}`);
|
|
1229
1466
|
this.name = "InstallTransactionError";
|
|
1230
1467
|
this.stage = options.stage;
|
|
1231
1468
|
this.causeError = options.cause;
|
|
@@ -1245,10 +1482,10 @@ function formatSuggestedFix(cause) {
|
|
|
1245
1482
|
if (cause instanceof FilePermissionError) {
|
|
1246
1483
|
return cause.suggestedFixCommand;
|
|
1247
1484
|
}
|
|
1248
|
-
const
|
|
1249
|
-
if (
|
|
1250
|
-
if (
|
|
1251
|
-
return `chmod u+w "${
|
|
1485
|
+
const nodeError = cause;
|
|
1486
|
+
if (nodeError?.code === "EACCES" || nodeError?.code === "EPERM") {
|
|
1487
|
+
if (nodeError.path) {
|
|
1488
|
+
return `chmod u+w "${nodeError.path}"`;
|
|
1252
1489
|
}
|
|
1253
1490
|
}
|
|
1254
1491
|
return void 0;
|
|
@@ -1262,18 +1499,23 @@ async function installSkills(options) {
|
|
|
1262
1499
|
if (!skillsToInstall.includes("odd-ai")) {
|
|
1263
1500
|
skillsToInstall.push("odd-ai");
|
|
1264
1501
|
}
|
|
1265
|
-
let currentStage = "
|
|
1502
|
+
let currentStage = "Preflight check";
|
|
1266
1503
|
try {
|
|
1504
|
+
runPreflightWriteCheck({
|
|
1505
|
+
targetPath: options.targetPath,
|
|
1506
|
+
selectedAgents: options.selectedAgents
|
|
1507
|
+
});
|
|
1508
|
+
currentStage = "Syncing skills store (.grooveskills/skills)";
|
|
1267
1509
|
syncProjectStore(options.targetPath, skillsToInstall, options.selectedAgents, transaction);
|
|
1268
1510
|
const projectSkillsDir = getProjectSkillsDir(options.targetPath);
|
|
1269
1511
|
const createdFiles = [];
|
|
1270
|
-
currentStage = "
|
|
1512
|
+
currentStage = "Configuring agent references (.agents, .claude, etc.)";
|
|
1271
1513
|
for (const skillId of skillsToInstall) {
|
|
1272
|
-
const skill = SKILL_CATALOG.find((
|
|
1514
|
+
const skill = SKILL_CATALOG.find((catalogItem) => catalogItem.id === skillId);
|
|
1273
1515
|
if (!skill) {
|
|
1274
1516
|
continue;
|
|
1275
1517
|
}
|
|
1276
|
-
const skillSourceDir =
|
|
1518
|
+
const skillSourceDir = join12(projectSkillsDir, skill.directoryName);
|
|
1277
1519
|
for (const agentId of options.selectedAgents) {
|
|
1278
1520
|
const adapter = ADAPTER_REGISTRY[agentId];
|
|
1279
1521
|
if (!adapter) {
|
|
@@ -1288,7 +1530,7 @@ async function installSkills(options) {
|
|
|
1288
1530
|
createdFiles.push(...files);
|
|
1289
1531
|
}
|
|
1290
1532
|
}
|
|
1291
|
-
currentStage = "
|
|
1533
|
+
currentStage = "Installing Groove Harness (context, decisions, tasks, and instructions)";
|
|
1292
1534
|
const harnessResult = installHarness({
|
|
1293
1535
|
targetPath: options.targetPath,
|
|
1294
1536
|
selectedAgents: options.selectedAgents,
|
|
@@ -1299,7 +1541,7 @@ async function installSkills(options) {
|
|
|
1299
1541
|
transaction
|
|
1300
1542
|
});
|
|
1301
1543
|
createdFiles.push(...harnessResult.createdFiles);
|
|
1302
|
-
currentStage = "
|
|
1544
|
+
currentStage = "Persisting project configuration (.grooveskills/config.json)";
|
|
1303
1545
|
const configPath = writeProjectConfig(
|
|
1304
1546
|
options.targetPath,
|
|
1305
1547
|
skillsToInstall,
|
|
@@ -1314,7 +1556,8 @@ async function installSkills(options) {
|
|
|
1314
1556
|
configuredAgents: options.selectedAgents,
|
|
1315
1557
|
createdFiles,
|
|
1316
1558
|
harnessInitialized: harnessResult.onboardingInitialized,
|
|
1317
|
-
language: options.language
|
|
1559
|
+
language: options.language,
|
|
1560
|
+
replacedFiles: transaction.getReplacedFiles()
|
|
1318
1561
|
};
|
|
1319
1562
|
} catch (cause) {
|
|
1320
1563
|
const rollbackReport = transaction.rollback();
|
|
@@ -1329,18 +1572,69 @@ async function installSkills(options) {
|
|
|
1329
1572
|
}
|
|
1330
1573
|
|
|
1331
1574
|
// src/domains/ui/render-onboarding-prompt.ts
|
|
1332
|
-
import
|
|
1575
|
+
import picocolors2 from "picocolors";
|
|
1576
|
+
|
|
1577
|
+
// src/domains/ui/terminal-layout.ts
|
|
1578
|
+
function getTerminalWidth(fallbackWidth = 80) {
|
|
1579
|
+
const terminalColumns = process.stdout.columns;
|
|
1580
|
+
if (typeof terminalColumns === "number" && terminalColumns > 20) {
|
|
1581
|
+
return Math.min(terminalColumns, 100);
|
|
1582
|
+
}
|
|
1583
|
+
return fallbackWidth;
|
|
1584
|
+
}
|
|
1585
|
+
function wrapText(rawText, maxLineWidth = 76) {
|
|
1586
|
+
return rawText.split("\n").map((line) => {
|
|
1587
|
+
if (line.length <= maxLineWidth) {
|
|
1588
|
+
return line;
|
|
1589
|
+
}
|
|
1590
|
+
const words = line.split(" ");
|
|
1591
|
+
const wrappedLines = [];
|
|
1592
|
+
let currentLine = "";
|
|
1593
|
+
for (const word of words) {
|
|
1594
|
+
const potentialLine = currentLine ? `${currentLine} ${word}` : word;
|
|
1595
|
+
if (potentialLine.length <= maxLineWidth) {
|
|
1596
|
+
currentLine = potentialLine;
|
|
1597
|
+
} else {
|
|
1598
|
+
if (currentLine) {
|
|
1599
|
+
wrappedLines.push(currentLine);
|
|
1600
|
+
}
|
|
1601
|
+
currentLine = word;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (currentLine) {
|
|
1605
|
+
wrappedLines.push(currentLine);
|
|
1606
|
+
}
|
|
1607
|
+
return wrappedLines.join("\n");
|
|
1608
|
+
}).join("\n");
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/domains/ui/render-onboarding-prompt.ts
|
|
1333
1612
|
function formatOnboardingPrompt(language = "es") {
|
|
1334
|
-
const
|
|
1335
|
-
const title =
|
|
1336
|
-
const prompt =
|
|
1337
|
-
const
|
|
1613
|
+
const languageConfig = SUPPORTED_LANGUAGES[language] ?? SUPPORTED_LANGUAGES.es;
|
|
1614
|
+
const title = languageConfig.onboardingPromptTitle;
|
|
1615
|
+
const prompt = languageConfig.onboardingPrompt;
|
|
1616
|
+
const boxWidth = Math.max(getTerminalWidth(80) - 4, 50);
|
|
1617
|
+
const contentWidth = boxWidth - 4;
|
|
1618
|
+
const wrappedPromptLines = wrapText(prompt, contentWidth).split("\n");
|
|
1619
|
+
const borderHorizontal = "\u2500".repeat(boxWidth - 2);
|
|
1620
|
+
const badgeText = " COPY & PASTE PROMPT FOR YOUR AI AGENT ";
|
|
1621
|
+
const badgePadded = badgeText.padEnd(contentWidth, " ");
|
|
1622
|
+
const topBorder = picocolors2.cyan(`\u256D${borderHorizontal}\u256E`);
|
|
1623
|
+
const badgeLine = `${picocolors2.cyan("\u2502 ")}${picocolors2.bold(picocolors2.bgCyan(picocolors2.black(badgePadded)))}${picocolors2.cyan(" \u2502")}`;
|
|
1624
|
+
const dividerLine = picocolors2.cyan(`\u251C${borderHorizontal}\u2524`);
|
|
1625
|
+
const promptLines = wrappedPromptLines.map((line) => {
|
|
1626
|
+
const paddedLine = line.padEnd(contentWidth, " ");
|
|
1627
|
+
return `${picocolors2.cyan("\u2502 ")}${picocolors2.bold(picocolors2.white(paddedLine))}${picocolors2.cyan(" \u2502")}`;
|
|
1628
|
+
});
|
|
1629
|
+
const bottomBorder = picocolors2.cyan(`\u2570${borderHorizontal}\u256F`);
|
|
1338
1630
|
const formattedOutput = [
|
|
1339
|
-
separator,
|
|
1340
|
-
pc2.bold(pc2.yellow(title)),
|
|
1341
1631
|
"",
|
|
1342
|
-
|
|
1343
|
-
|
|
1632
|
+
topBorder,
|
|
1633
|
+
badgeLine,
|
|
1634
|
+
dividerLine,
|
|
1635
|
+
...promptLines,
|
|
1636
|
+
bottomBorder,
|
|
1637
|
+
""
|
|
1344
1638
|
].join("\n");
|
|
1345
1639
|
return {
|
|
1346
1640
|
title,
|
|
@@ -1349,75 +1643,133 @@ function formatOnboardingPrompt(language = "es") {
|
|
|
1349
1643
|
};
|
|
1350
1644
|
}
|
|
1351
1645
|
|
|
1646
|
+
// src/domains/ui/render-install-report.ts
|
|
1647
|
+
import { basename as basename2 } from "path";
|
|
1648
|
+
import { userInfo } from "os";
|
|
1649
|
+
import * as prompts5 from "@clack/prompts";
|
|
1650
|
+
import picocolors3 from "picocolors";
|
|
1651
|
+
function getCurrentUsername() {
|
|
1652
|
+
try {
|
|
1653
|
+
return userInfo().username;
|
|
1654
|
+
} catch {
|
|
1655
|
+
return process.env.USER || process.env.LOGNAME || "current user";
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function formatReplacedFileNotes(replacedFiles = []) {
|
|
1659
|
+
if (replacedFiles.length === 0) {
|
|
1660
|
+
return [];
|
|
1661
|
+
}
|
|
1662
|
+
const currentUser = getCurrentUsername();
|
|
1663
|
+
return replacedFiles.map((replacedItem) => {
|
|
1664
|
+
const fileName = basename2(replacedItem.filePath);
|
|
1665
|
+
const previousOwner = formatOwnerName(replacedItem.previousOwnerUid);
|
|
1666
|
+
return picocolors3.yellow(
|
|
1667
|
+
`Note: ${fileName} was owned by ${previousOwner}. It was replaced atomically and is now owned by ${currentUser}.`
|
|
1668
|
+
);
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
function renderInstallFailure(error) {
|
|
1672
|
+
const { stage, causeError, rollbackReport, suggestedFix } = error;
|
|
1673
|
+
const causeMessage = causeError instanceof Error ? causeError.message : String(causeError);
|
|
1674
|
+
const nothingTouched = rollbackReport.restoredFiles.length === 0 && rollbackReport.removedFiles.length === 0 && rollbackReport.removedDirs.length === 0 && rollbackReport.failures.length === 0;
|
|
1675
|
+
const safeMaxWidth = Math.max(getTerminalWidth(80) - 10, 50);
|
|
1676
|
+
const lines = [
|
|
1677
|
+
wrapText(`${picocolors3.bold("Failed stage:")} ${picocolors3.yellow(stage)}`, safeMaxWidth),
|
|
1678
|
+
wrapText(`${picocolors3.bold("Cause:")} ${picocolors3.red(causeMessage)}`, safeMaxWidth)
|
|
1679
|
+
];
|
|
1680
|
+
if (nothingTouched) {
|
|
1681
|
+
lines.push(`${picocolors3.bold("Rollback:")} Nothing to roll back, no files were modified.`);
|
|
1682
|
+
} else {
|
|
1683
|
+
lines.push(
|
|
1684
|
+
"",
|
|
1685
|
+
picocolors3.bold("Rollback summary:"),
|
|
1686
|
+
` \u2022 Restored files: ${picocolors3.cyan(String(rollbackReport.restoredFiles.length))}`,
|
|
1687
|
+
` \u2022 Removed temporary files: ${picocolors3.cyan(String(rollbackReport.removedFiles.length))}`,
|
|
1688
|
+
` \u2022 Removed empty directories: ${picocolors3.cyan(String(rollbackReport.removedDirs.length))}`
|
|
1689
|
+
);
|
|
1690
|
+
if (!rollbackReport.isClean) {
|
|
1691
|
+
lines.push("", picocolors3.red("Rollback was incomplete. These files may need manual review:"));
|
|
1692
|
+
for (const failure of rollbackReport.failures) {
|
|
1693
|
+
lines.push(wrapText(` \u2022 ${failure.path} (${failure.reason})`, safeMaxWidth));
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
if (suggestedFix) {
|
|
1698
|
+
lines.push(wrapText(`${picocolors3.bold("Suggested fix:")} ${picocolors3.green(suggestedFix)}`, safeMaxWidth));
|
|
1699
|
+
}
|
|
1700
|
+
prompts5.note(lines.join("\n"), "Installation Aborted");
|
|
1701
|
+
if (!rollbackReport.isClean) {
|
|
1702
|
+
prompts5.outro(
|
|
1703
|
+
picocolors3.red(
|
|
1704
|
+
"Rollback was incomplete. Please review the files listed above and apply the suggested fix."
|
|
1705
|
+
)
|
|
1706
|
+
);
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
prompts5.outro(
|
|
1710
|
+
picocolors3.yellow(
|
|
1711
|
+
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."
|
|
1712
|
+
)
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1352
1716
|
// src/domains/ui/ui-orchestrator.ts
|
|
1353
1717
|
async function resolveLanguageSelection(harnessExists, configuredLanguage, forceOnboarding) {
|
|
1354
1718
|
if (harnessExists && !forceOnboarding) {
|
|
1355
1719
|
return configuredLanguage ?? "es";
|
|
1356
1720
|
}
|
|
1357
|
-
const
|
|
1358
|
-
if (
|
|
1359
|
-
|
|
1721
|
+
const languageSelection = await promptLanguage(configuredLanguage ?? "es");
|
|
1722
|
+
if (prompts6.isCancel(languageSelection)) {
|
|
1723
|
+
prompts6.cancel("Installation cancelled. Have a wonderful day!");
|
|
1360
1724
|
process.exit(0);
|
|
1361
1725
|
}
|
|
1362
|
-
return
|
|
1726
|
+
return languageSelection;
|
|
1363
1727
|
}
|
|
1364
1728
|
async function resolveOverwriteConfirmation(targetPath, selectedSkills, selectedAgents) {
|
|
1365
1729
|
const anyExists = selectedSkills.some((skillId) => {
|
|
1366
|
-
const
|
|
1367
|
-
return
|
|
1730
|
+
const skillMetadata = SKILL_CATALOG.find((skill) => skill.id === skillId);
|
|
1731
|
+
return skillMetadata ? verifySkillExists(targetPath, skillMetadata.directoryName, selectedAgents) : false;
|
|
1368
1732
|
});
|
|
1369
1733
|
if (!anyExists) {
|
|
1370
1734
|
return true;
|
|
1371
1735
|
}
|
|
1372
|
-
const
|
|
1373
|
-
if (
|
|
1374
|
-
|
|
1736
|
+
const overwriteConfirmed = await promptConfirmOverwrite();
|
|
1737
|
+
if (prompts6.isCancel(overwriteConfirmed) || !overwriteConfirmed) {
|
|
1738
|
+
prompts6.cancel("Installation stopped to protect existing skills.");
|
|
1375
1739
|
process.exit(0);
|
|
1376
1740
|
}
|
|
1377
|
-
return Boolean(
|
|
1378
|
-
}
|
|
1379
|
-
function renderInstallFailure(error) {
|
|
1380
|
-
const { stage, causeError, rollbackReport, suggestedFix } = error;
|
|
1381
|
-
const rawMsg = causeError instanceof Error ? causeError.message : String(causeError);
|
|
1382
|
-
const lines = [
|
|
1383
|
-
`${pc3.bold("Fase afectada:")} ${pc3.yellow(stage)}`,
|
|
1384
|
-
`${pc3.bold("Causa:")} ${pc3.red(rawMsg)}`,
|
|
1385
|
-
"",
|
|
1386
|
-
pc3.bold("Reporte de Rollback autom\xE1tico:"),
|
|
1387
|
-
` \u2022 Archivos restaurados a su estado previo: ${pc3.cyan(String(rollbackReport.restoredFiles.length))}`,
|
|
1388
|
-
` \u2022 Archivos temporales eliminados: ${pc3.cyan(String(rollbackReport.removedFiles.length))}`,
|
|
1389
|
-
` \u2022 Directorios vac\xEDos eliminados: ${pc3.cyan(String(rollbackReport.removedDirs.length))}`
|
|
1390
|
-
];
|
|
1391
|
-
if (suggestedFix) {
|
|
1392
|
-
lines.push(
|
|
1393
|
-
"",
|
|
1394
|
-
pc3.bold("Soluci\xF3n recomendada:"),
|
|
1395
|
-
` Ejecuta en tu terminal: ${pc3.green(suggestedFix)}`
|
|
1396
|
-
);
|
|
1397
|
-
}
|
|
1398
|
-
p5.note(lines.join("\n"), "Detalles de la interrupci\xF3n y reversi\xF3n");
|
|
1399
|
-
p5.outro(pc3.yellow("Tu proyecto no sufri\xF3 alteraciones y se preserv\xF3 intacto. Corrige la causa y vuelve a intentarlo cuando gustes."));
|
|
1741
|
+
return Boolean(overwriteConfirmed);
|
|
1400
1742
|
}
|
|
1401
1743
|
async function runInteractiveUi(context, forceOnboarding = false) {
|
|
1402
1744
|
console.log(renderBanner());
|
|
1403
|
-
|
|
1745
|
+
prompts6.intro(
|
|
1746
|
+
picocolors4.magenta(
|
|
1747
|
+
"Welcome to GrooveSkills! Let's tailor your AI agents with clean architectural standards."
|
|
1748
|
+
)
|
|
1749
|
+
);
|
|
1750
|
+
const safeMaxWidth = Math.max(getTerminalWidth(80) - 10, 50);
|
|
1404
1751
|
if (context.detectedDependencies.length > 0) {
|
|
1405
|
-
const harnessInfo = context.harnessExists && !forceOnboarding ?
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1752
|
+
const harnessInfo = context.harnessExists && !forceOnboarding ? picocolors4.green(
|
|
1753
|
+
`Active (.grooveskills/memory, lang: ${context.configuredLanguage ?? "es"})`
|
|
1754
|
+
) : picocolors4.cyan("Will be initialized");
|
|
1755
|
+
const formattedDependencies = context.detectedDependencies.map((dependencyName) => picocolors4.bold(dependencyName)).join(", ");
|
|
1756
|
+
const formattedAgents = context.detectedAgents.map((agentName) => picocolors4.green(agentName)).join(", ") || "None (will be created)";
|
|
1757
|
+
const environmentNote = wrapText(
|
|
1758
|
+
`Detected dependencies: ${formattedDependencies}
|
|
1759
|
+
Active AI folders: ${formattedAgents}
|
|
1409
1760
|
GrooveSkills Harness: ${harnessInfo}`,
|
|
1410
|
-
|
|
1761
|
+
safeMaxWidth
|
|
1411
1762
|
);
|
|
1763
|
+
prompts6.note(environmentNote, "Project Environment");
|
|
1412
1764
|
}
|
|
1413
1765
|
const selectedSkills = await promptSkills(context.suggestedSkillIds);
|
|
1414
|
-
if (
|
|
1415
|
-
|
|
1766
|
+
if (prompts6.isCancel(selectedSkills)) {
|
|
1767
|
+
prompts6.cancel("Installation cancelled. Have a wonderful day!");
|
|
1416
1768
|
process.exit(0);
|
|
1417
1769
|
}
|
|
1418
1770
|
const selectedAgents = await promptAgents(context.detectedAgents);
|
|
1419
|
-
if (
|
|
1420
|
-
|
|
1771
|
+
if (prompts6.isCancel(selectedAgents)) {
|
|
1772
|
+
prompts6.cancel("Installation cancelled. Have a wonderful day!");
|
|
1421
1773
|
process.exit(0);
|
|
1422
1774
|
}
|
|
1423
1775
|
const selectedLanguage = await resolveLanguageSelection(
|
|
@@ -1430,10 +1782,10 @@ GrooveSkills Harness: ${harnessInfo}`,
|
|
|
1430
1782
|
selectedSkills,
|
|
1431
1783
|
selectedAgents
|
|
1432
1784
|
);
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1785
|
+
const installSpinner = prompts6.spinner();
|
|
1786
|
+
installSpinner.start("Installing and adapting skills across your configured AI agents...");
|
|
1435
1787
|
try {
|
|
1436
|
-
const
|
|
1788
|
+
const installResult = await installSkills({
|
|
1437
1789
|
targetPath: context.targetPath,
|
|
1438
1790
|
selectedSkillIds: selectedSkills,
|
|
1439
1791
|
selectedAgents,
|
|
@@ -1443,24 +1795,32 @@ GrooveSkills Harness: ${harnessInfo}`,
|
|
|
1443
1795
|
language: selectedLanguage,
|
|
1444
1796
|
forceOnboarding
|
|
1445
1797
|
});
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
p5.note(
|
|
1449
|
-
`Skills configuradas: ${result.installedSkills.map((sk) => pc3.bold(sk)).join(", ")}
|
|
1450
|
-
Agentes objetivo: ${result.configuredAgents.map((a) => pc3.cyan(a)).join(", ")}
|
|
1451
|
-
GrooveSkills Harness: ${harnessStatus}`,
|
|
1452
|
-
"Resumen de Instalaci\xF3n"
|
|
1798
|
+
installSpinner.stop(
|
|
1799
|
+
picocolors4.green("All skills and harness have been successfully configured!")
|
|
1453
1800
|
);
|
|
1454
|
-
|
|
1801
|
+
const harnessStatus = installResult.harnessInitialized ? picocolors4.green(`Initialized in .grooveskills/ (Language: ${selectedLanguage})`) : picocolors4.cyan(`Preserved in .grooveskills/ (Language: ${selectedLanguage})`);
|
|
1802
|
+
const formattedInstalledSkills = installResult.installedSkills.map((skillId) => picocolors4.bold(skillId)).join(", ");
|
|
1803
|
+
const formattedConfiguredAgents = installResult.configuredAgents.map((agentId) => picocolors4.cyan(agentId)).join(", ");
|
|
1804
|
+
const summaryLines = [
|
|
1805
|
+
wrapText(`Configured skills: ${formattedInstalledSkills}`, safeMaxWidth),
|
|
1806
|
+
`Target agents: ${formattedConfiguredAgents}`,
|
|
1807
|
+
`GrooveSkills Harness: ${harnessStatus}`,
|
|
1808
|
+
...formatReplacedFileNotes(installResult.replacedFiles).map(
|
|
1809
|
+
(note3) => wrapText(note3, safeMaxWidth)
|
|
1810
|
+
)
|
|
1811
|
+
];
|
|
1812
|
+
prompts6.note(summaryLines.join("\n"), "Installation Summary");
|
|
1813
|
+
if (installResult.harnessInitialized) {
|
|
1455
1814
|
const promptRender = formatOnboardingPrompt(selectedLanguage);
|
|
1456
|
-
|
|
1457
|
-
pc3.green(promptRender.prompt),
|
|
1458
|
-
promptRender.title
|
|
1459
|
-
);
|
|
1815
|
+
console.log(promptRender.formattedOutput);
|
|
1460
1816
|
}
|
|
1461
|
-
|
|
1817
|
+
prompts6.outro(
|
|
1818
|
+
picocolors4.green(
|
|
1819
|
+
"Your AI programming pair is now empowered with your team's architectural habits! \u{1F680}"
|
|
1820
|
+
)
|
|
1821
|
+
);
|
|
1462
1822
|
} catch (error) {
|
|
1463
|
-
|
|
1823
|
+
installSpinner.stop(picocolors4.yellow("Installation could not be completed."));
|
|
1464
1824
|
if (error instanceof InstallTransactionError) {
|
|
1465
1825
|
renderInstallFailure(error);
|
|
1466
1826
|
return;
|
|
@@ -1470,6 +1830,13 @@ GrooveSkills Harness: ${harnessStatus}`,
|
|
|
1470
1830
|
}
|
|
1471
1831
|
|
|
1472
1832
|
// src/main.ts
|
|
1833
|
+
function resolveUsername() {
|
|
1834
|
+
try {
|
|
1835
|
+
return userInfo2().username;
|
|
1836
|
+
} catch {
|
|
1837
|
+
return process.env.USER || process.env.LOGNAME || "current user";
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1473
1840
|
async function runGrooveSkills(options = {}) {
|
|
1474
1841
|
const targetPath = process.cwd();
|
|
1475
1842
|
const context = detectProjectContext(targetPath);
|
|
@@ -1489,6 +1856,18 @@ async function runGrooveSkills(options = {}) {
|
|
|
1489
1856
|
language,
|
|
1490
1857
|
forceOnboarding: options.forceOnboarding
|
|
1491
1858
|
});
|
|
1859
|
+
if (result.replacedFiles && result.replacedFiles.length > 0) {
|
|
1860
|
+
const currentUser = resolveUsername();
|
|
1861
|
+
for (const replacedItem of result.replacedFiles) {
|
|
1862
|
+
const fileName = basename3(replacedItem.filePath);
|
|
1863
|
+
const previousOwner = formatOwnerName(replacedItem.previousOwnerUid);
|
|
1864
|
+
console.log(
|
|
1865
|
+
picocolors5.yellow(
|
|
1866
|
+
`Note: ${fileName} was owned by ${previousOwner}. It was replaced atomically and is now owned by ${currentUser}.`
|
|
1867
|
+
)
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1492
1871
|
if (result.harnessInitialized) {
|
|
1493
1872
|
const promptRender = formatOnboardingPrompt(language);
|
|
1494
1873
|
console.log(`
|
|
@@ -1504,4 +1883,4 @@ export {
|
|
|
1504
1883
|
InstallTransactionError,
|
|
1505
1884
|
runGrooveSkills
|
|
1506
1885
|
};
|
|
1507
|
-
//# sourceMappingURL=chunk-
|
|
1886
|
+
//# sourceMappingURL=chunk-KPDHH2AZ.js.map
|