cli-jaw 1.4.4 → 1.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/bin/commands/browser.js +3 -2
- package/dist/bin/commands/browser.js.map +1 -1
- package/dist/bin/commands/doctor.js +39 -5
- package/dist/bin/commands/doctor.js.map +1 -1
- package/dist/bin/commands/memory.js +3 -2
- package/dist/bin/commands/memory.js.map +1 -1
- package/dist/bin/commands/orchestrate.js +5 -4
- package/dist/bin/commands/orchestrate.js.map +1 -1
- package/dist/bin/postinstall.js +25 -9
- package/dist/bin/postinstall.js.map +1 -1
- package/dist/lib/mcp-sync.js +256 -137
- package/dist/lib/mcp-sync.js.map +1 -1
- package/dist/server.js +12 -8
- package/dist/server.js.map +1 -1
- package/dist/src/agent/spawn.js +6 -0
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/cli/command-context.js +2 -2
- package/dist/src/cli/command-context.js.map +1 -1
- package/dist/src/core/config.js +3 -2
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +3 -0
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/prompt/templates/a1-system.md +1 -0
- package/package.json +1 -1
- package/scripts/release.sh +3 -0
package/dist/lib/mcp-sync.js
CHANGED
|
@@ -250,79 +250,171 @@ export function syncToAll(config) {
|
|
|
250
250
|
}
|
|
251
251
|
return results;
|
|
252
252
|
}
|
|
253
|
-
|
|
253
|
+
function buildLinkReport(links, extra) {
|
|
254
|
+
const summary = links.reduce((acc, item) => {
|
|
255
|
+
const key = item.action || 'unknown';
|
|
256
|
+
acc[key] = (acc[key] || 0) + 1;
|
|
257
|
+
return acc;
|
|
258
|
+
}, {});
|
|
259
|
+
return { links, summary, ...extra };
|
|
260
|
+
}
|
|
254
261
|
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* Also ensure {workingDir}/.claude/skills + ~/.claude/skills (Claude Code CLI)
|
|
262
|
+
* Working-dir only: {workingDir}/.agents/skills, optionally .claude/skills.
|
|
263
|
+
* NEVER touches home shared paths (~/.agents, ~/.agent, ~/.claude).
|
|
258
264
|
*/
|
|
259
|
-
export function
|
|
260
|
-
const
|
|
261
|
-
const skillsSource = join(
|
|
265
|
+
export function ensureWorkingDirSkillsLinks(workingDir, opts = {}) {
|
|
266
|
+
const { _homedir = os.homedir(), _jawHome = JAW_HOME, onConflict = 'skip', includeClaude = false } = opts;
|
|
267
|
+
const skillsSource = join(_jawHome, 'skills');
|
|
262
268
|
fs.mkdirSync(skillsSource, { recursive: true });
|
|
263
|
-
|
|
269
|
+
// CRITICAL: workingDir === homedir → skip to prevent implicit shared path creation
|
|
270
|
+
const resolvedWd = resolve(workingDir);
|
|
271
|
+
if (resolvedWd === resolve(_homedir)) {
|
|
272
|
+
return buildLinkReport([], {
|
|
273
|
+
skipped: true,
|
|
274
|
+
reason: 'workingDir is homedir — use ensureSharedHomeSkillsLinks() for explicit opt-in',
|
|
275
|
+
source: skillsSource,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
const { allowReplaceManaged = false } = opts;
|
|
279
|
+
const backupContext = createBackupContext(_jawHome);
|
|
280
|
+
const safeOpts = { onConflict, backupContext, allowReplaceManaged, jawHome: _jawHome };
|
|
264
281
|
const links = [];
|
|
265
|
-
// 1. {workingDir}/.agents/skills →
|
|
282
|
+
// 1. {workingDir}/.agents/skills → skills source
|
|
266
283
|
const wdLink = join(workingDir, '.agents', 'skills');
|
|
267
|
-
links.push(ensureSymlinkSafe(skillsSource, wdLink, {
|
|
268
|
-
// 2.
|
|
269
|
-
|
|
270
|
-
|
|
284
|
+
links.push(ensureSymlinkSafe(skillsSource, wdLink, { ...safeOpts, name: 'wdAgents' }));
|
|
285
|
+
// 2. Optionally {workingDir}/.claude/skills → skills source
|
|
286
|
+
if (includeClaude) {
|
|
287
|
+
const wdClaudeSkills = join(workingDir, '.claude', 'skills');
|
|
288
|
+
links.push(ensureSymlinkSafe(skillsSource, wdClaudeSkills, { ...safeOpts, name: 'wdClaude' }));
|
|
289
|
+
}
|
|
290
|
+
return buildLinkReport(links, {
|
|
291
|
+
skipped: false,
|
|
292
|
+
source: skillsSource,
|
|
293
|
+
strategy: onConflict,
|
|
294
|
+
backupRoot: backupContext.root,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Opt-in only: create shared home symlinks (~/.agents/skills, ~/.agent/skills, ~/.claude/skills).
|
|
299
|
+
* Must NEVER be called by default — only via explicit env flag or CLI command.
|
|
300
|
+
*/
|
|
301
|
+
export function ensureSharedHomeSkillsLinks(opts = {}) {
|
|
302
|
+
const { _homedir = os.homedir(), _jawHome = JAW_HOME, onConflict = 'backup', includeAgents = true, includeCompatAgent = true, includeClaude = true, } = opts;
|
|
303
|
+
const skillsSource = join(_jawHome, 'skills');
|
|
304
|
+
fs.mkdirSync(skillsSource, { recursive: true });
|
|
305
|
+
const backupContext = createBackupContext(_jawHome);
|
|
306
|
+
const links = [];
|
|
307
|
+
if (includeAgents) {
|
|
308
|
+
const homeLink = join(_homedir, '.agents', 'skills');
|
|
271
309
|
links.push(ensureSymlinkSafe(skillsSource, homeLink, { onConflict, backupContext, name: 'homeAgents' }));
|
|
272
310
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
name: 'homeAgents',
|
|
278
|
-
linkPath: homeLink,
|
|
279
|
-
target: skillsSource,
|
|
280
|
-
});
|
|
311
|
+
if (includeCompatAgent) {
|
|
312
|
+
const homeAgents = join(_homedir, '.agents', 'skills');
|
|
313
|
+
const compatLink = join(_homedir, '.agent', 'skills');
|
|
314
|
+
links.push(ensureSymlinkSafe(homeAgents, compatLink, { onConflict, backupContext, name: 'compatAgent' }));
|
|
281
315
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
links.push(ensureSymlinkSafe(homeLink, compatLink, { onConflict, backupContext, name: 'compatAgent' }));
|
|
285
|
-
// 4. Claude Code CLI: {workingDir}/.claude/skills → ~/.cli-jaw/skills
|
|
286
|
-
const wdClaudeSkills = join(workingDir, '.claude', 'skills');
|
|
287
|
-
links.push(ensureSymlinkSafe(skillsSource, wdClaudeSkills, { onConflict, backupContext, name: 'wdClaude' }));
|
|
288
|
-
// 5. Home Claude Code: ~/.claude/skills → ~/.cli-jaw/skills
|
|
289
|
-
const homeClaudeSkills = join(os.homedir(), '.claude', 'skills');
|
|
290
|
-
if (homeClaudeSkills !== wdClaudeSkills) {
|
|
316
|
+
if (includeClaude) {
|
|
317
|
+
const homeClaudeSkills = join(_homedir, '.claude', 'skills');
|
|
291
318
|
links.push(ensureSymlinkSafe(skillsSource, homeClaudeSkills, { onConflict, backupContext, name: 'homeClaude' }));
|
|
292
319
|
}
|
|
293
|
-
|
|
294
|
-
links.push({
|
|
295
|
-
status: 'skip',
|
|
296
|
-
action: 'same_path',
|
|
297
|
-
name: 'homeClaude',
|
|
298
|
-
linkPath: homeClaudeSkills,
|
|
299
|
-
target: skillsSource,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
const summary = links.reduce((acc, item) => {
|
|
303
|
-
const key = item.action || 'unknown';
|
|
304
|
-
acc[key] = (acc[key] || 0) + 1;
|
|
305
|
-
return acc;
|
|
306
|
-
}, {});
|
|
307
|
-
return {
|
|
320
|
+
return buildLinkReport(links, {
|
|
308
321
|
source: skillsSource,
|
|
309
322
|
strategy: onConflict,
|
|
310
323
|
backupRoot: backupContext.root,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Detect shared path contamination: check if cli-jaw has taken over home shared paths.
|
|
328
|
+
*/
|
|
329
|
+
export function detectSharedPathContamination(opts) {
|
|
330
|
+
const homedir = opts?._homedir ?? os.homedir();
|
|
331
|
+
const jawHome = opts?._jawHome ?? JAW_HOME;
|
|
332
|
+
const skillsTarget = join(jawHome, 'skills');
|
|
333
|
+
const sharedPaths = [
|
|
334
|
+
join(homedir, '.agents', 'skills'),
|
|
335
|
+
join(homedir, '.agent', 'skills'),
|
|
336
|
+
join(homedir, '.claude', 'skills'),
|
|
337
|
+
];
|
|
338
|
+
const paths = sharedPaths.map(p => {
|
|
339
|
+
const exists = fs.existsSync(p);
|
|
340
|
+
let isSymlink = false;
|
|
341
|
+
let target = null;
|
|
342
|
+
let isCliJaw = false;
|
|
343
|
+
if (exists) {
|
|
344
|
+
try {
|
|
345
|
+
const stat = fs.lstatSync(p);
|
|
346
|
+
isSymlink = stat.isSymbolicLink();
|
|
347
|
+
if (isSymlink) {
|
|
348
|
+
const rawTarget = fs.readlinkSync(p);
|
|
349
|
+
target = resolveSymlinkTarget(p, rawTarget);
|
|
350
|
+
isCliJaw = resolve(target) === resolve(skillsTarget);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch { /* ignore */ }
|
|
354
|
+
}
|
|
355
|
+
return { path: p, exists, isSymlink, target, isCliJaw };
|
|
356
|
+
});
|
|
357
|
+
// Check backup traces
|
|
358
|
+
const backupDir = join(jawHome, 'backups', 'skills-conflicts');
|
|
359
|
+
const backupTraces = [];
|
|
360
|
+
if (fs.existsSync(backupDir)) {
|
|
361
|
+
try {
|
|
362
|
+
backupTraces.push(...fs.readdirSync(backupDir).map(f => join(backupDir, f)));
|
|
363
|
+
}
|
|
364
|
+
catch { /* ignore */ }
|
|
365
|
+
}
|
|
366
|
+
const contaminated = paths.filter(p => p.isCliJaw);
|
|
367
|
+
let status = 'clean';
|
|
368
|
+
let summary = 'No shared path contamination detected';
|
|
369
|
+
if (contaminated.length > 0) {
|
|
370
|
+
status = 'contaminated';
|
|
371
|
+
const pathList = contaminated.map(p => p.path).join(', ');
|
|
372
|
+
summary = `cli-jaw symlinks found at shared paths: ${pathList}`;
|
|
373
|
+
}
|
|
374
|
+
else if (backupTraces.length > 0) {
|
|
375
|
+
// Backup traces without active symlinks = previously resolved, not active contamination
|
|
376
|
+
status = 'resolved';
|
|
377
|
+
summary = `No active symlinks; backup traces preserved for rollback (${backupTraces.length} file(s))`;
|
|
378
|
+
}
|
|
379
|
+
return { status, paths, backupTraces, summary };
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* @deprecated Use ensureWorkingDirSkillsLinks or ensureSharedHomeSkillsLinks instead.
|
|
383
|
+
* Kept only for backward compatibility during transition — delegates to new helpers.
|
|
384
|
+
*/
|
|
385
|
+
export function ensureSkillsSymlinks(workingDir, opts = {}) {
|
|
386
|
+
// Delegate to working-dir-only helper (isolated-by-default)
|
|
387
|
+
return ensureWorkingDirSkillsLinks(workingDir, {
|
|
388
|
+
onConflict: opts.onConflict === 'skip' ? 'skip' : 'backup',
|
|
389
|
+
includeClaude: true,
|
|
390
|
+
});
|
|
314
391
|
}
|
|
315
|
-
function createBackupContext() {
|
|
392
|
+
function createBackupContext(jawHome) {
|
|
316
393
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
317
|
-
return { root: join(JAW_HOME, 'backups', 'skills-conflicts', stamp) };
|
|
394
|
+
return { root: join(jawHome ?? JAW_HOME, 'backups', 'skills-conflicts', stamp) };
|
|
318
395
|
}
|
|
319
396
|
function resolveSymlinkTarget(linkPath, rawTarget) {
|
|
320
397
|
return isAbsolute(rawTarget)
|
|
321
398
|
? resolve(rawTarget)
|
|
322
399
|
: resolve(dirname(linkPath), rawTarget);
|
|
323
400
|
}
|
|
401
|
+
function isCliJawManaged(linkPath, jawHome) {
|
|
402
|
+
try {
|
|
403
|
+
const stat = fs.lstatSync(linkPath);
|
|
404
|
+
if (stat.isSymbolicLink()) {
|
|
405
|
+
const rawTarget = fs.readlinkSync(linkPath);
|
|
406
|
+
const currentTarget = resolveSymlinkTarget(linkPath, rawTarget);
|
|
407
|
+
// Symlink pointing into any .cli-jaw directory is managed
|
|
408
|
+
return jawHome ? currentTarget.startsWith(resolve(jawHome)) : currentTarget.includes('.cli-jaw');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch { /* not a symlink or doesn't exist */ }
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
324
414
|
function ensureSymlinkSafe(target, linkPath, opts = {}) {
|
|
325
415
|
const onConflict = opts.onConflict === 'skip' ? 'skip' : 'backup';
|
|
416
|
+
const allowReplaceManaged = opts.allowReplaceManaged === true;
|
|
417
|
+
const jawHome = opts.jawHome;
|
|
326
418
|
const backupContext = opts.backupContext || createBackupContext();
|
|
327
419
|
const absTarget = resolve(target);
|
|
328
420
|
const baseResult = {
|
|
@@ -338,18 +430,31 @@ function ensureSymlinkSafe(target, linkPath, opts = {}) {
|
|
|
338
430
|
if (currentTarget === absTarget) {
|
|
339
431
|
return { ...baseResult, status: 'ok', action: 'noop' };
|
|
340
432
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
433
|
+
// Stale symlink: only replace if managed by cli-jaw AND caller opts in
|
|
434
|
+
const managed = isCliJawManaged(linkPath, jawHome);
|
|
435
|
+
if (managed && allowReplaceManaged) {
|
|
436
|
+
fs.unlinkSync(linkPath);
|
|
437
|
+
fs.mkdirSync(dirname(linkPath), { recursive: true });
|
|
438
|
+
fs.symlinkSync(target, linkPath);
|
|
439
|
+
console.log(`[skills] symlink(updated): ${linkPath} → ${target}`);
|
|
440
|
+
return {
|
|
441
|
+
...baseResult,
|
|
442
|
+
status: 'ok',
|
|
443
|
+
action: 'replace_symlink',
|
|
444
|
+
previousTarget: rawTarget,
|
|
445
|
+
managed,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
// Not managed, respect onConflict
|
|
449
|
+
if (onConflict === 'skip') {
|
|
450
|
+
console.warn(`[skills] conflict(skip): ${linkPath} (unmanaged symlink preserved)`);
|
|
451
|
+
return { ...baseResult, status: 'skip', action: 'conflict_skip' };
|
|
452
|
+
}
|
|
453
|
+
// backup mode: fall through to backup_replace below
|
|
351
454
|
}
|
|
352
455
|
if (onConflict === 'skip') {
|
|
456
|
+
// Non-symlink path: check if allowReplaceManaged applies
|
|
457
|
+
// (real dirs are never "managed" — only symlinks can be reliably attributed to cli-jaw)
|
|
353
458
|
console.warn(`[skills] conflict(skip): ${linkPath} (existing path preserved)`);
|
|
354
459
|
return { ...baseResult, status: 'skip', action: 'conflict_skip' };
|
|
355
460
|
}
|
|
@@ -543,7 +648,7 @@ const CODEX_ACTIVE = new Set([
|
|
|
543
648
|
const OPENCLAW_ACTIVE = new Set([
|
|
544
649
|
'browser', 'notion', 'memory', 'vision-click',
|
|
545
650
|
'screen-capture', 'docx', 'xlsx', 'pptx', 'hwp', 'github', 'telegram-send',
|
|
546
|
-
'video',
|
|
651
|
+
'video', 'pdf-vision',
|
|
547
652
|
]);
|
|
548
653
|
function getSkillVersion(id, registry) {
|
|
549
654
|
return registry?.skills?.[id]?.version ?? null;
|
|
@@ -610,31 +715,41 @@ export function copyDefaultSkills() {
|
|
|
610
715
|
console.log(`[skills] Codex: not installed, using bundled fallback`);
|
|
611
716
|
}
|
|
612
717
|
// ─── 2. Populate skills_ref/ ─────────────────────
|
|
613
|
-
// Priority:
|
|
718
|
+
// Priority: git clone (always latest) → bundled fallback (dev) → offline
|
|
614
719
|
const packageRefDir = join(findPackageRoot(), 'skills_ref');
|
|
615
720
|
const SKILLS_REPO = 'https://github.com/lidge-jun/cli-jaw-skills.git';
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
721
|
+
let skillsSourceResolved = false;
|
|
722
|
+
// 2a. Try GitHub clone first (public repo, no auth needed)
|
|
723
|
+
try {
|
|
724
|
+
const tmpClone = join(JAW_HOME, '.skills_clone_tmp');
|
|
725
|
+
if (fs.existsSync(tmpClone))
|
|
726
|
+
fs.rmSync(tmpClone, { recursive: true });
|
|
727
|
+
console.log(`[skills] fetching latest skills from GitHub...`);
|
|
728
|
+
execSync(`git clone --depth 1 ${SKILLS_REPO} "${tmpClone}"`, {
|
|
729
|
+
stdio: 'pipe', timeout: 120000,
|
|
730
|
+
});
|
|
731
|
+
// Version-aware merge from clone
|
|
732
|
+
const srcReg = loadRegistry(tmpClone);
|
|
619
733
|
const dstReg = loadRegistry(refDir);
|
|
620
|
-
const
|
|
621
|
-
let
|
|
622
|
-
for (const entry of
|
|
623
|
-
|
|
734
|
+
const cloned = fs.readdirSync(tmpClone, { withFileTypes: true });
|
|
735
|
+
let cloneNew = 0, cloneUpdated = 0;
|
|
736
|
+
for (const entry of cloned) {
|
|
737
|
+
if (entry.name === '.git')
|
|
738
|
+
continue;
|
|
739
|
+
const src = join(tmpClone, entry.name);
|
|
624
740
|
const dst = join(refDir, entry.name);
|
|
625
741
|
if (entry.isDirectory()) {
|
|
626
742
|
if (!fs.existsSync(dst)) {
|
|
627
743
|
copyDirRecursive(src, dst);
|
|
628
|
-
|
|
744
|
+
cloneNew++;
|
|
629
745
|
}
|
|
630
746
|
else {
|
|
631
747
|
const sv = getSkillVersion(entry.name, srcReg);
|
|
632
748
|
const dv = getSkillVersion(entry.name, dstReg);
|
|
633
|
-
// Migration: dv===null means pre-version install → always update
|
|
634
749
|
if (sv && (!dv || semverGt(sv, dv))) {
|
|
635
750
|
fs.rmSync(dst, { recursive: true, force: true });
|
|
636
751
|
copyDirRecursive(src, dst);
|
|
637
|
-
|
|
752
|
+
cloneUpdated++;
|
|
638
753
|
console.log(`[skills] updated: ${entry.name} ${dv ?? '(none)'} → ${sv}`);
|
|
639
754
|
}
|
|
640
755
|
}
|
|
@@ -643,36 +758,28 @@ export function copyDefaultSkills() {
|
|
|
643
758
|
fs.copyFileSync(src, dst);
|
|
644
759
|
}
|
|
645
760
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
console.log(`[skills] Bundled: ${refUpdated} skills updated`);
|
|
761
|
+
fs.rmSync(tmpClone, { recursive: true, force: true });
|
|
762
|
+
console.log(`[skills] ✅ GitHub: ${cloneNew} new, ${cloneUpdated} updated`);
|
|
763
|
+
skillsSourceResolved = true;
|
|
650
764
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
execSync(`git clone --depth 1 ${SKILLS_REPO} "${tmpClone}"`, {
|
|
660
|
-
stdio: 'pipe', timeout: 120000,
|
|
661
|
-
});
|
|
662
|
-
// Version-aware merge (same logic as bundled path)
|
|
663
|
-
const srcReg = loadRegistry(tmpClone);
|
|
765
|
+
catch (e) {
|
|
766
|
+
console.log(`[skills] GitHub clone skipped: ${e.message?.slice(0, 60)}`);
|
|
767
|
+
}
|
|
768
|
+
// 2b. Fallback: bundled skills_ref/ (dev mode with initialized submodule)
|
|
769
|
+
if (!skillsSourceResolved) {
|
|
770
|
+
const bundledHasContent = fs.existsSync(packageRefDir) && fs.existsSync(join(packageRefDir, 'registry.json'));
|
|
771
|
+
if (bundledHasContent) {
|
|
772
|
+
const srcReg = loadRegistry(packageRefDir);
|
|
664
773
|
const dstReg = loadRegistry(refDir);
|
|
665
|
-
const
|
|
666
|
-
let
|
|
667
|
-
for (const entry of
|
|
668
|
-
|
|
669
|
-
continue;
|
|
670
|
-
const src = join(tmpClone, entry.name);
|
|
774
|
+
const entries = fs.readdirSync(packageRefDir, { withFileTypes: true });
|
|
775
|
+
let refCopied = 0, refUpdated = 0;
|
|
776
|
+
for (const entry of entries) {
|
|
777
|
+
const src = join(packageRefDir, entry.name);
|
|
671
778
|
const dst = join(refDir, entry.name);
|
|
672
779
|
if (entry.isDirectory()) {
|
|
673
780
|
if (!fs.existsSync(dst)) {
|
|
674
781
|
copyDirRecursive(src, dst);
|
|
675
|
-
|
|
782
|
+
refCopied++;
|
|
676
783
|
}
|
|
677
784
|
else {
|
|
678
785
|
const sv = getSkillVersion(entry.name, srcReg);
|
|
@@ -680,7 +787,7 @@ export function copyDefaultSkills() {
|
|
|
680
787
|
if (sv && (!dv || semverGt(sv, dv))) {
|
|
681
788
|
fs.rmSync(dst, { recursive: true, force: true });
|
|
682
789
|
copyDirRecursive(src, dst);
|
|
683
|
-
|
|
790
|
+
refUpdated++;
|
|
684
791
|
console.log(`[skills] updated: ${entry.name} ${dv ?? '(none)'} → ${sv}`);
|
|
685
792
|
}
|
|
686
793
|
}
|
|
@@ -689,17 +796,18 @@ export function copyDefaultSkills() {
|
|
|
689
796
|
fs.copyFileSync(src, dst);
|
|
690
797
|
}
|
|
691
798
|
}
|
|
692
|
-
|
|
693
|
-
|
|
799
|
+
if (refCopied > 0)
|
|
800
|
+
console.log(`[skills] Bundled fallback: ${refCopied} new skills → ref`);
|
|
801
|
+
if (refUpdated > 0)
|
|
802
|
+
console.log(`[skills] Bundled fallback: ${refUpdated} skills updated`);
|
|
803
|
+
skillsSourceResolved = true;
|
|
694
804
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
console.log(`[skills] update check skipped: ${e.message?.slice(0, 60)}`);
|
|
702
|
-
}
|
|
805
|
+
}
|
|
806
|
+
if (!skillsSourceResolved) {
|
|
807
|
+
const hasExisting = fs.existsSync(join(refDir, 'registry.json'));
|
|
808
|
+
if (!hasExisting) {
|
|
809
|
+
console.warn(`[skills] ⚠️ no source available (no network + no bundled skills)`);
|
|
810
|
+
console.warn(`[skills] offline mode — skills will be available after 'jaw init'`);
|
|
703
811
|
}
|
|
704
812
|
}
|
|
705
813
|
// ─── 3. Auto-activate from refDir ───────────────
|
|
@@ -757,45 +865,56 @@ export function softResetSkills() {
|
|
|
757
865
|
const activeDir = join(JAW_HOME, 'skills');
|
|
758
866
|
const refDir = join(JAW_HOME, 'skills_ref');
|
|
759
867
|
const packageRefDir = join(findPackageRoot(), 'skills_ref');
|
|
760
|
-
// 1. Source for ref update:
|
|
761
|
-
|
|
868
|
+
// 1. Source for ref update: GitHub clone (latest) → bundled fallback (dev)
|
|
869
|
+
const SKILLS_REPO = 'https://github.com/lidge-jun/cli-jaw-skills.git';
|
|
870
|
+
let sourceDir = null;
|
|
762
871
|
let tmpCloneDir = null;
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const SKILLS_REPO = 'https://github.com/lidge-jun/cli-jaw-skills.git';
|
|
872
|
+
// 1a. Try GitHub clone first (public repo, always latest)
|
|
873
|
+
try {
|
|
766
874
|
tmpCloneDir = join(JAW_HOME, '.skills_clone_tmp');
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
875
|
+
if (fs.existsSync(tmpCloneDir))
|
|
876
|
+
fs.rmSync(tmpCloneDir, { recursive: true });
|
|
877
|
+
console.log(`[skills:soft-reset] fetching latest skills from GitHub...`);
|
|
878
|
+
execSync(`git clone --depth 1 ${SKILLS_REPO} "${tmpCloneDir}"`, {
|
|
879
|
+
stdio: 'pipe', timeout: 120000,
|
|
880
|
+
});
|
|
881
|
+
sourceDir = tmpCloneDir;
|
|
882
|
+
}
|
|
883
|
+
catch (e) {
|
|
884
|
+
console.log(`[skills:soft-reset] GitHub clone skipped: ${e.message?.slice(0, 60)}`);
|
|
885
|
+
tmpCloneDir = null;
|
|
886
|
+
}
|
|
887
|
+
// 1b. Fallback: bundled skills_ref/ (dev mode with initialized submodule)
|
|
888
|
+
if (!sourceDir) {
|
|
889
|
+
const bundledReady = fs.existsSync(packageRefDir) && fs.existsSync(join(packageRefDir, 'registry.json'));
|
|
890
|
+
if (bundledReady) {
|
|
891
|
+
sourceDir = packageRefDir;
|
|
892
|
+
console.log(`[skills:soft-reset] using bundled fallback`);
|
|
775
893
|
}
|
|
776
|
-
|
|
777
|
-
console.warn(`[skills:soft-reset] ⚠️
|
|
778
|
-
console.warn(`[skills:soft-reset] keeping current skills unchanged`);
|
|
894
|
+
else {
|
|
895
|
+
console.warn(`[skills:soft-reset] ⚠️ no source available — keeping current skills unchanged`);
|
|
779
896
|
return { restored: 0, added: 0 };
|
|
780
897
|
}
|
|
781
898
|
}
|
|
782
899
|
// 2. skills_ref/ 전체를 소스에서 다시 복사 (덮어쓰기)
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
if (
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
fs.copyFileSync(src, dst);
|
|
796
|
-
}
|
|
900
|
+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
901
|
+
if (entry.name === '.git')
|
|
902
|
+
continue;
|
|
903
|
+
const src = join(sourceDir, entry.name);
|
|
904
|
+
const dst = join(refDir, entry.name);
|
|
905
|
+
if (entry.isDirectory()) {
|
|
906
|
+
if (fs.existsSync(dst))
|
|
907
|
+
fs.rmSync(dst, { recursive: true, force: true });
|
|
908
|
+
copyDirRecursive(src, dst);
|
|
909
|
+
}
|
|
910
|
+
else if (entry.isFile()) {
|
|
911
|
+
fs.copyFileSync(src, dst);
|
|
797
912
|
}
|
|
798
913
|
}
|
|
914
|
+
// Cleanup temp clone
|
|
915
|
+
if (tmpCloneDir && fs.existsSync(tmpCloneDir)) {
|
|
916
|
+
fs.rmSync(tmpCloneDir, { recursive: true, force: true });
|
|
917
|
+
}
|
|
799
918
|
// 3. active skills → ref에 같은 이름이 있으면 무조건 덮어쓰기
|
|
800
919
|
let restored = 0;
|
|
801
920
|
if (fs.existsSync(activeDir)) {
|