@sushanglewis/lincoln 1.8.0 → 1.8.3

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,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/commands/install.ts
4
- import fs5 from "fs";
5
- import path6 from "path";
4
+ import fs6 from "fs";
5
+ import path7 from "path";
6
6
  import { spawn as spawn2 } from "child_process";
7
7
 
8
8
  // src/lib/paths.ts
@@ -106,7 +106,12 @@ var COPY_SUBDIRS = [
106
106
  "stages",
107
107
  "schemas",
108
108
  "templates",
109
- "harnesses"
109
+ "harnesses",
110
+ // Policies are data files consumed by hooks at runtime (e.g.
111
+ // .claude/policies/less-is-more.md read by on-session-start.sh,
112
+ // security.yaml read by pre-tool-use.sh). Without syncing them into the
113
+ // project, vendored installs silently skip those guardrails (#126).
114
+ "policies"
110
115
  ];
111
116
  function sha256(filePath) {
112
117
  const data = fs3.readFileSync(filePath);
@@ -327,9 +332,265 @@ async function syncHarness(harnessId, opts, syncClaude) {
327
332
  return report;
328
333
  }
329
334
 
330
- // src/lib/harnessDetect.ts
335
+ // src/lib/syncSkills.ts
331
336
  import fs4 from "fs";
332
337
  import path5 from "path";
338
+ import { execFile } from "child_process";
339
+ import yaml from "js-yaml";
340
+
341
+ // src/lib/semver.ts
342
+ var SEMVER_CORE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
343
+ var NUMERIC_IDENTIFIER = /^\d+$/;
344
+ function compareVersions(a, b) {
345
+ const pa = parseVersion(a);
346
+ const pb = parseVersion(b);
347
+ if (!pa || !pb) {
348
+ return void 0;
349
+ }
350
+ if (pa.major !== pb.major) return pa.major - pb.major;
351
+ if (pa.minor !== pb.minor) return pa.minor - pb.minor;
352
+ if (pa.patch !== pb.patch) return pa.patch - pb.patch;
353
+ return comparePrerelease(pa.prerelease, pb.prerelease);
354
+ }
355
+ function parseVersion(version) {
356
+ const match = SEMVER_CORE.exec(version.trim());
357
+ if (!match) {
358
+ return void 0;
359
+ }
360
+ return {
361
+ major: Number(match[1]),
362
+ minor: Number(match[2]),
363
+ patch: Number(match[3]),
364
+ prerelease: match[4] ? match[4].split(".") : []
365
+ };
366
+ }
367
+ function comparePrerelease(a, b) {
368
+ if (a.length === 0 && b.length === 0) return 0;
369
+ if (a.length === 0) return 1;
370
+ if (b.length === 0) return -1;
371
+ const shared = Math.min(a.length, b.length);
372
+ for (let i = 0; i < shared; i++) {
373
+ const x = a[i];
374
+ const y = b[i];
375
+ if (x === y) continue;
376
+ const xNumeric = NUMERIC_IDENTIFIER.test(x);
377
+ const yNumeric = NUMERIC_IDENTIFIER.test(y);
378
+ if (xNumeric && yNumeric) return Number(x) - Number(y);
379
+ if (xNumeric) return -1;
380
+ if (yNumeric) return 1;
381
+ return x < y ? -1 : 1;
382
+ }
383
+ return a.length - b.length;
384
+ }
385
+
386
+ // src/lib/syncSkills.ts
387
+ var GIT_SOURCE = /^(https?:\/\/|git@)/;
388
+ var FULL_SHA = /^[0-9a-f]{40}$/i;
389
+ var PINNED_SEMVER = /^v?(\d+\.\d+\.\d+)$/;
390
+ var NPM_GLOBAL_INSTALL = /^npm install -g (\S+)$/;
391
+ var MAX_BUFFER = 16 * 1024 * 1024;
392
+ var defaultRunner = (cmd, args) => new Promise((resolvePromise, rejectPromise) => {
393
+ execFile(cmd, args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
394
+ if (err && !("code" in err && typeof err.code === "number")) {
395
+ rejectPromise(err);
396
+ return;
397
+ }
398
+ const code = err && typeof err.code === "number" ? err.code : 0;
399
+ resolvePromise({ code, stdout: String(stdout), stderr: String(stderr) });
400
+ });
401
+ });
402
+ async function syncExternalSkills(options) {
403
+ const report = { entries: [], warnings: [] };
404
+ const manifestPath = path5.join(options.payloadRoot, ".claude", "skills", "dependencies.yaml");
405
+ if (!fs4.existsSync(manifestPath)) {
406
+ report.warnings.push(`skill dependency manifest not found at ${manifestPath}; skipping external skill sync`);
407
+ return report;
408
+ }
409
+ let manifest;
410
+ try {
411
+ manifest = yaml.load(fs4.readFileSync(manifestPath, "utf8")) ?? {};
412
+ } catch (err) {
413
+ report.warnings.push(
414
+ `failed to parse ${manifestPath}: ${err instanceof Error ? err.message : String(err)}`
415
+ );
416
+ return report;
417
+ }
418
+ const runCommand3 = options.runCommand ?? defaultRunner;
419
+ const platform = options.platform ?? (process.platform === "darwin" ? "macos" : process.platform === "linux" ? "linux" : process.platform);
420
+ for (const [name, cfg] of Object.entries(manifest.skills ?? {})) {
421
+ if (!cfg.source || cfg.source === "inline" || !GIT_SOURCE.test(cfg.source)) {
422
+ continue;
423
+ }
424
+ const enabled = cfg.default_install ?? cfg.required ?? true;
425
+ if (!enabled) {
426
+ report.entries.push({ name, status: "skipped-optional", detail: "default_install is false" });
427
+ continue;
428
+ }
429
+ if (cfg.type === "cli") {
430
+ await syncCli(name, cfg, options, platform, runCommand3, report);
431
+ } else {
432
+ await syncGitSkill(name, cfg, options, runCommand3, report);
433
+ }
434
+ }
435
+ return report;
436
+ }
437
+ async function syncGitSkill(name, cfg, options, runCommand3, report) {
438
+ const source = cfg.source;
439
+ const ref = cfg.ref ?? "";
440
+ const target = path5.join(options.skillsDir, name);
441
+ if (options.dryRun) {
442
+ const planned = fs4.existsSync(target) ? "verify pinned ref and update if behind" : `clone and pin to ${ref}`;
443
+ report.entries.push({ name, status: "dry-run", detail: `would ${planned}` });
444
+ return;
445
+ }
446
+ if (!fs4.existsSync(target)) {
447
+ const clone = await runCommand3("git", ["clone", source, target]);
448
+ if (clone.code !== 0) {
449
+ fail(report, name, `git clone failed: ${clone.stderr.trim() || `exit code ${clone.code}`}`);
450
+ return;
451
+ }
452
+ const checkout = await runCommand3("git", ["-C", target, "checkout", ref]);
453
+ if (checkout.code !== 0) {
454
+ fail(report, name, `git checkout ${ref} failed: ${checkout.stderr.trim() || `exit code ${checkout.code}`}`);
455
+ return;
456
+ }
457
+ report.entries.push({ name, status: "installed", detail: ref });
458
+ return;
459
+ }
460
+ const status = await runCommand3("git", ["-C", target, "status", "--porcelain"]);
461
+ if (status.code !== 0) {
462
+ fail(report, name, `git status failed (not a git repository?): ${status.stderr.trim() || `exit code ${status.code}`}`);
463
+ return;
464
+ }
465
+ if (status.stdout.trim() !== "") {
466
+ const detail = "working tree is dirty; skipping update (commit or stash local changes first)";
467
+ report.entries.push({ name, status: "skipped-dirty", detail });
468
+ report.warnings.push(`${name}: ${detail}`);
469
+ return;
470
+ }
471
+ const head = await runCommand3("git", ["-C", target, "rev-parse", "HEAD"]);
472
+ if (head.code !== 0) {
473
+ fail(report, name, `git rev-parse failed: ${head.stderr.trim() || `exit code ${head.code}`}`);
474
+ return;
475
+ }
476
+ if (head.stdout.trim() === ref) {
477
+ report.entries.push({ name, status: "current", detail: ref });
478
+ return;
479
+ }
480
+ const advertised = await isPinAdvertised(source, ref, runCommand3);
481
+ if (advertised) {
482
+ const fetch2 = await runCommand3("git", ["-C", target, "fetch", "origin", ref]);
483
+ if (fetch2.code === 0 && await checkoutFetchHead(name, target, runCommand3, report)) {
484
+ return;
485
+ }
486
+ if (fetch2.code !== 0) {
487
+ fail(report, name, `git fetch origin ${ref} failed: ${fetch2.stderr.trim() || `exit code ${fetch2.code}`}`);
488
+ return;
489
+ }
490
+ return;
491
+ }
492
+ const fetch = await runCommand3("git", ["-C", target, "fetch", "origin", ref]);
493
+ if (fetch.code === 0) {
494
+ await checkoutFetchHead(name, target, runCommand3, report);
495
+ return;
496
+ }
497
+ if (FULL_SHA.test(ref)) {
498
+ const fetchAll = await runCommand3("git", ["-C", target, "fetch", "origin"]);
499
+ if (fetchAll.code !== 0) {
500
+ fail(report, name, `git fetch origin failed: ${fetchAll.stderr.trim() || `exit code ${fetchAll.code}`}`);
501
+ return;
502
+ }
503
+ const checkout = await runCommand3("git", ["-C", target, "checkout", ref]);
504
+ if (checkout.code !== 0) {
505
+ fail(report, name, `git checkout ${ref} failed: ${checkout.stderr.trim() || `exit code ${checkout.code}`}`);
506
+ return;
507
+ }
508
+ report.entries.push({ name, status: "updated", detail: ref });
509
+ return;
510
+ }
511
+ fail(report, name, `git fetch origin ${ref} failed: ${fetch.stderr.trim() || `exit code ${fetch.code}`}`);
512
+ }
513
+ async function isPinAdvertised(source, ref, runCommand3) {
514
+ const remote = await runCommand3("git", ["ls-remote", source]);
515
+ if (remote.code !== 0) {
516
+ return false;
517
+ }
518
+ return remote.stdout.split("\n").some((line) => line.split(" ")[0]?.trim() === ref);
519
+ }
520
+ async function checkoutFetchHead(name, target, runCommand3, report) {
521
+ const checkout = await runCommand3("git", ["-C", target, "checkout", "FETCH_HEAD"]);
522
+ if (checkout.code !== 0) {
523
+ fail(report, name, `git checkout FETCH_HEAD failed: ${checkout.stderr.trim() || `exit code ${checkout.code}`}`);
524
+ return false;
525
+ }
526
+ report.entries.push({ name, status: "updated", detail: "checked out FETCH_HEAD" });
527
+ return true;
528
+ }
529
+ async function syncCli(name, cfg, options, platform, runCommand3, report) {
530
+ const binary = cfg.binary ?? name;
531
+ const installCommand = cfg.platforms?.[platform] ?? cfg.platforms?.linux;
532
+ const npmPackage = installCommand ? parseNpmGlobalInstall(installCommand) : void 0;
533
+ const pinned = cfg.ref ? pinnedSemver(cfg.ref) : void 0;
534
+ if (options.dryRun) {
535
+ const managed = npmPackage && pinned ? `install ${npmPackage}@${pinned} if missing or older` : "verify presence";
536
+ report.entries.push({ name, status: "dry-run", detail: `would ${managed}` });
537
+ return;
538
+ }
539
+ const whichCmd = process.platform === "win32" ? "where" : "which";
540
+ const found = await runCommand3(whichCmd, [binary]);
541
+ if (found.code !== 0) {
542
+ if (npmPackage && pinned) {
543
+ await npmInstall(name, npmPackage, pinned, runCommand3, report, "installed");
544
+ return;
545
+ }
546
+ fail(report, name, `CLI '${binary}' not found. Install manually (${platform}): ${installCommand ?? "see dependency manifest"}`);
547
+ return;
548
+ }
549
+ if (!npmPackage || !pinned) {
550
+ report.entries.push({ name, status: "current", detail: `${binary} present` });
551
+ return;
552
+ }
553
+ const version = await runCommand3(binary, ["--version"]);
554
+ const installed = version.code === 0 ? version.stdout.split("\n")[0]?.trim() ?? "" : "";
555
+ const comparison = installed ? compareVersions(installed, pinned) : void 0;
556
+ if (comparison === void 0) {
557
+ report.entries.push({
558
+ name,
559
+ status: "current",
560
+ detail: `could not parse installed version '${installed}'; leaving ${binary} as-is`
561
+ });
562
+ return;
563
+ }
564
+ if (comparison >= 0) {
565
+ report.entries.push({ name, status: "current", detail: `${binary} ${installed}` });
566
+ return;
567
+ }
568
+ await npmInstall(name, npmPackage, pinned, runCommand3, report, "updated");
569
+ }
570
+ async function npmInstall(name, npmPackage, version, runCommand3, report, status) {
571
+ const result = await runCommand3("npm", ["install", "-g", `${npmPackage}@${version}`]);
572
+ if (result.code !== 0) {
573
+ fail(report, name, `npm install -g ${npmPackage}@${version} failed: ${result.stderr.trim() || `exit code ${result.code}`}`);
574
+ return;
575
+ }
576
+ report.entries.push({ name, status, detail: `${npmPackage}@${version}` });
577
+ }
578
+ function parseNpmGlobalInstall(command) {
579
+ const match = NPM_GLOBAL_INSTALL.exec(command.trim());
580
+ return match?.[1];
581
+ }
582
+ function pinnedSemver(ref) {
583
+ const match = PINNED_SEMVER.exec(ref.trim());
584
+ return match?.[0].replace(/^v/, "");
585
+ }
586
+ function fail(report, name, detail) {
587
+ report.entries.push({ name, status: "failed", detail });
588
+ report.warnings.push(`${name}: ${detail}`);
589
+ }
590
+
591
+ // src/lib/harnessDetect.ts
592
+ import fs5 from "fs";
593
+ import path6 from "path";
333
594
  import os2 from "os";
334
595
  var HARNESS_SPECS = [
335
596
  {
@@ -343,9 +604,9 @@ var HARNESS_SPECS = [
343
604
  function detectGlobalHarnesses(overrideHomeDir) {
344
605
  const homeDir = overrideHomeDir ?? os2.homedir();
345
606
  return HARNESS_SPECS.map((spec) => {
346
- const configDir = path5.join(homeDir, spec.configDirName);
347
- const installed = fs4.existsSync(configDir);
348
- const pluginManaged = spec.pluginMarker ? fs4.existsSync(path5.join(configDir, spec.pluginMarker)) : false;
607
+ const configDir = path6.join(homeDir, spec.configDirName);
608
+ const installed = fs5.existsSync(configDir);
609
+ const pluginManaged = spec.pluginMarker ? fs5.existsSync(path6.join(configDir, spec.pluginMarker)) : false;
349
610
  return {
350
611
  id: spec.id,
351
612
  installed,
@@ -537,6 +798,7 @@ function createDefaultDeps() {
537
798
  paths: resolveLincolnPaths(),
538
799
  payloadRoot: resolvePayloadRoot() || "",
539
800
  syncHarnesses,
801
+ syncExternalSkills,
540
802
  createPrompt,
541
803
  resolvePythonForVenv,
542
804
  isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY)
@@ -549,7 +811,7 @@ async function install(options, deps = createDefaultDeps()) {
549
811
  return 1;
550
812
  }
551
813
  const payloadRoot = deps.payloadRoot;
552
- if (!payloadRoot || !fs5.existsSync(payloadRoot)) {
814
+ if (!payloadRoot || !fs6.existsSync(payloadRoot)) {
553
815
  console.error('Lincoln payload not found. Run "npm install -g @sushanglewis/lincoln" first.');
554
816
  return 1;
555
817
  }
@@ -568,15 +830,15 @@ async function install(options, deps = createDefaultDeps()) {
568
830
  return 1;
569
831
  }
570
832
  if (!options.dryRun) {
571
- const versionDir = path6.join(deps.paths.versionsDir, version);
572
- fs5.mkdirSync(versionDir, { recursive: true });
573
- fs5.cpSync(payloadRoot, versionDir, { recursive: true, force: true });
833
+ const versionDir = path7.join(deps.paths.versionsDir, version);
834
+ fs6.mkdirSync(versionDir, { recursive: true });
835
+ fs6.cpSync(payloadRoot, versionDir, { recursive: true, force: true });
574
836
  try {
575
- fs5.rmSync(deps.paths.currentDir, { recursive: true, force: true });
576
- fs5.symlinkSync(versionDir, deps.paths.currentDir);
837
+ fs6.rmSync(deps.paths.currentDir, { recursive: true, force: true });
838
+ fs6.symlinkSync(versionDir, deps.paths.currentDir);
577
839
  } catch {
578
- fs5.rmSync(deps.paths.currentDir, { recursive: true, force: true });
579
- fs5.cpSync(versionDir, deps.paths.currentDir, { recursive: true, force: true });
840
+ fs6.rmSync(deps.paths.currentDir, { recursive: true, force: true });
841
+ fs6.cpSync(versionDir, deps.paths.currentDir, { recursive: true, force: true });
580
842
  }
581
843
  }
582
844
  const reports = await deps.syncHarnesses({
@@ -596,9 +858,30 @@ async function install(options, deps = createDefaultDeps()) {
596
858
  managedFiles: collectManagedFiles(reports)
597
859
  });
598
860
  }
599
- printSummary(options, version, harnesses, reports);
861
+ const skillReport = await syncSkillLibraries(options, deps, harnesses);
862
+ printSummary(options, version, harnesses, reports, skillReport);
600
863
  return 0;
601
864
  }
865
+ async function syncSkillLibraries(options, deps, harnesses) {
866
+ if (!harnesses.includes("claude-code") || !deps.syncExternalSkills) {
867
+ return void 0;
868
+ }
869
+ const payloadRoot = deps.paths.currentDir;
870
+ const manifestPath = path7.join(payloadRoot, ".claude", "skills", "dependencies.yaml");
871
+ if (!fs6.existsSync(manifestPath)) {
872
+ return void 0;
873
+ }
874
+ try {
875
+ return await deps.syncExternalSkills({
876
+ payloadRoot,
877
+ skillsDir: path7.join(deps.paths.claudeDir, "skills"),
878
+ dryRun: options.dryRun
879
+ });
880
+ } catch (err) {
881
+ console.warn(` warning: external skill sync failed: ${err instanceof Error ? err.message : String(err)}`);
882
+ return void 0;
883
+ }
884
+ }
602
885
  async function resolvePythonIfNeeded(options, deps, harnesses) {
603
886
  const needsPython = !options.dryRun && !options.noVenv && harnesses.some((id) => id === "codex" || id === "opencode");
604
887
  if (!needsPython) {
@@ -659,7 +942,7 @@ function collectManagedFiles(reports) {
659
942
  }
660
943
  return files;
661
944
  }
662
- function printSummary(options, version, harnesses, reports) {
945
+ function printSummary(options, version, harnesses, reports, skillReport) {
663
946
  const warnings = collectWarnings(reports);
664
947
  if (options.dryRun) {
665
948
  console.log(`Dry run: would install Lincoln ${version} for harnesses: ${harnesses.join(", ")}`);
@@ -670,6 +953,13 @@ function printSummary(options, version, harnesses, reports) {
670
953
  const count = report.written.length + report.skipped.length + report.preserved.length;
671
954
  console.log(` ${harnessId}: ${count} file(s)`);
672
955
  }
956
+ if (skillReport) {
957
+ for (const entry of skillReport.entries) {
958
+ const detail = entry.detail ? ` (${entry.detail})` : "";
959
+ console.log(` skill ${entry.name}: ${entry.status}${detail}`);
960
+ }
961
+ warnings.push(...skillReport.warnings);
962
+ }
673
963
  for (const warning of warnings) {
674
964
  console.warn(` warning: ${warning}`);
675
965
  }
@@ -730,7 +1020,7 @@ async function resolvePythonForVenv(envOverride = process.env.LINCOLN_PYTHON, ca
730
1020
  return void 0;
731
1021
  }
732
1022
  function venvPythonPath(paths) {
733
- return path6.join(
1023
+ return path7.join(
734
1024
  paths.venvDir,
735
1025
  process.platform === "win32" ? "Scripts" : "bin",
736
1026
  process.platform === "win32" ? "python.exe" : "python"
@@ -738,17 +1028,17 @@ function venvPythonPath(paths) {
738
1028
  }
739
1029
  async function setupVenv(paths, payloadRoot, pythonCommand) {
740
1030
  const pythonExe = venvPythonPath(paths);
741
- if (!fs5.existsSync(pythonExe)) {
1031
+ if (!fs6.existsSync(pythonExe)) {
742
1032
  console.log("Creating Lincoln virtual environment...");
743
1033
  const venvCode = await runCommand2(pythonCommand, ["-m", "venv", paths.venvDir]);
744
1034
  if (venvCode !== 0) {
745
1035
  throw new Error(`Failed to create virtual environment (exit code ${venvCode})`);
746
1036
  }
747
1037
  }
748
- const requirementsPath = path6.join(payloadRoot, "requirements.txt");
749
- if (fs5.existsSync(requirementsPath)) {
1038
+ const requirementsPath = path7.join(payloadRoot, "requirements.txt");
1039
+ if (fs6.existsSync(requirementsPath)) {
750
1040
  console.log("Installing Python dependencies...");
751
- const pipExe = path6.join(
1041
+ const pipExe = path7.join(
752
1042
  paths.venvDir,
753
1043
  process.platform === "win32" ? "Scripts" : "bin",
754
1044
  process.platform === "win32" ? "pip.exe" : "pip"