@sushanglewis/lincoln 1.7.0 → 1.8.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.
Files changed (28) hide show
  1. package/dist/main.js +347 -107
  2. package/dist/main.js.map +1 -1
  3. package/dist/postinstall.js +306 -21
  4. package/dist/postinstall.js.map +1 -1
  5. package/framework/.claude/skills/build-product-prototype/prompts/main.md +6 -1
  6. package/framework/.claude/skills/clarify-requirements/prompts/main.md +6 -0
  7. package/framework/.claude/skills/dependencies.yaml +8 -8
  8. package/framework/.claude/skills/draft-product-design/SKILL.md +1 -0
  9. package/framework/.claude/skills/draft-product-design/prompts/main.md +26 -3
  10. package/framework/.claude/skills/lc-workflow/validators/validate.py +1 -0
  11. package/framework/.claude/stages/product-design-docs.yaml +2 -0
  12. package/framework/.claude/workflows/bug-fix.yaml +1 -0
  13. package/framework/.claude/workflows/design-spike.yaml +1 -0
  14. package/framework/.claude/workflows/existing-project-iteration.yaml +1 -0
  15. package/framework/.claude/workflows/interview-to-knowledge.yaml +1 -0
  16. package/framework/.claude/workflows/oss-first-design.yaml +1 -0
  17. package/framework/.claude-plugin/marketplace.json +2 -2
  18. package/framework/.claude-plugin/plugin.json +1 -1
  19. package/framework/.version-bump.json +1 -1
  20. package/framework/README.en.md +21 -3
  21. package/framework/README.md +23 -3
  22. package/framework/RELEASE.md +41 -1
  23. package/framework/manifest.json +24 -24
  24. package/framework/scripts/lincoln_index.py +8 -1
  25. package/framework/tools/lincoln/package-lock.json +2 -2
  26. package/framework/tools/lincoln/package.json +1 -1
  27. package/framework/tools/lincoln-installer/package.json +1 -1
  28. package/package.json +1 -1
@@ -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
@@ -327,9 +327,265 @@ async function syncHarness(harnessId, opts, syncClaude) {
327
327
  return report;
328
328
  }
329
329
 
330
- // src/lib/harnessDetect.ts
330
+ // src/lib/syncSkills.ts
331
331
  import fs4 from "fs";
332
332
  import path5 from "path";
333
+ import { execFile } from "child_process";
334
+ import yaml from "js-yaml";
335
+
336
+ // src/lib/semver.ts
337
+ var SEMVER_CORE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
338
+ var NUMERIC_IDENTIFIER = /^\d+$/;
339
+ function compareVersions(a, b) {
340
+ const pa = parseVersion(a);
341
+ const pb = parseVersion(b);
342
+ if (!pa || !pb) {
343
+ return void 0;
344
+ }
345
+ if (pa.major !== pb.major) return pa.major - pb.major;
346
+ if (pa.minor !== pb.minor) return pa.minor - pb.minor;
347
+ if (pa.patch !== pb.patch) return pa.patch - pb.patch;
348
+ return comparePrerelease(pa.prerelease, pb.prerelease);
349
+ }
350
+ function parseVersion(version) {
351
+ const match = SEMVER_CORE.exec(version.trim());
352
+ if (!match) {
353
+ return void 0;
354
+ }
355
+ return {
356
+ major: Number(match[1]),
357
+ minor: Number(match[2]),
358
+ patch: Number(match[3]),
359
+ prerelease: match[4] ? match[4].split(".") : []
360
+ };
361
+ }
362
+ function comparePrerelease(a, b) {
363
+ if (a.length === 0 && b.length === 0) return 0;
364
+ if (a.length === 0) return 1;
365
+ if (b.length === 0) return -1;
366
+ const shared = Math.min(a.length, b.length);
367
+ for (let i = 0; i < shared; i++) {
368
+ const x = a[i];
369
+ const y = b[i];
370
+ if (x === y) continue;
371
+ const xNumeric = NUMERIC_IDENTIFIER.test(x);
372
+ const yNumeric = NUMERIC_IDENTIFIER.test(y);
373
+ if (xNumeric && yNumeric) return Number(x) - Number(y);
374
+ if (xNumeric) return -1;
375
+ if (yNumeric) return 1;
376
+ return x < y ? -1 : 1;
377
+ }
378
+ return a.length - b.length;
379
+ }
380
+
381
+ // src/lib/syncSkills.ts
382
+ var GIT_SOURCE = /^(https?:\/\/|git@)/;
383
+ var FULL_SHA = /^[0-9a-f]{40}$/i;
384
+ var PINNED_SEMVER = /^v?(\d+\.\d+\.\d+)$/;
385
+ var NPM_GLOBAL_INSTALL = /^npm install -g (\S+)$/;
386
+ var MAX_BUFFER = 16 * 1024 * 1024;
387
+ var defaultRunner = (cmd, args) => new Promise((resolvePromise, rejectPromise) => {
388
+ execFile(cmd, args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
389
+ if (err && !("code" in err && typeof err.code === "number")) {
390
+ rejectPromise(err);
391
+ return;
392
+ }
393
+ const code = err && typeof err.code === "number" ? err.code : 0;
394
+ resolvePromise({ code, stdout: String(stdout), stderr: String(stderr) });
395
+ });
396
+ });
397
+ async function syncExternalSkills(options) {
398
+ const report = { entries: [], warnings: [] };
399
+ const manifestPath = path5.join(options.payloadRoot, ".claude", "skills", "dependencies.yaml");
400
+ if (!fs4.existsSync(manifestPath)) {
401
+ report.warnings.push(`skill dependency manifest not found at ${manifestPath}; skipping external skill sync`);
402
+ return report;
403
+ }
404
+ let manifest;
405
+ try {
406
+ manifest = yaml.load(fs4.readFileSync(manifestPath, "utf8")) ?? {};
407
+ } catch (err) {
408
+ report.warnings.push(
409
+ `failed to parse ${manifestPath}: ${err instanceof Error ? err.message : String(err)}`
410
+ );
411
+ return report;
412
+ }
413
+ const runCommand3 = options.runCommand ?? defaultRunner;
414
+ const platform = options.platform ?? (process.platform === "darwin" ? "macos" : process.platform === "linux" ? "linux" : process.platform);
415
+ for (const [name, cfg] of Object.entries(manifest.skills ?? {})) {
416
+ if (!cfg.source || cfg.source === "inline" || !GIT_SOURCE.test(cfg.source)) {
417
+ continue;
418
+ }
419
+ const enabled = cfg.default_install ?? cfg.required ?? true;
420
+ if (!enabled) {
421
+ report.entries.push({ name, status: "skipped-optional", detail: "default_install is false" });
422
+ continue;
423
+ }
424
+ if (cfg.type === "cli") {
425
+ await syncCli(name, cfg, options, platform, runCommand3, report);
426
+ } else {
427
+ await syncGitSkill(name, cfg, options, runCommand3, report);
428
+ }
429
+ }
430
+ return report;
431
+ }
432
+ async function syncGitSkill(name, cfg, options, runCommand3, report) {
433
+ const source = cfg.source;
434
+ const ref = cfg.ref ?? "";
435
+ const target = path5.join(options.skillsDir, name);
436
+ if (options.dryRun) {
437
+ const planned = fs4.existsSync(target) ? "verify pinned ref and update if behind" : `clone and pin to ${ref}`;
438
+ report.entries.push({ name, status: "dry-run", detail: `would ${planned}` });
439
+ return;
440
+ }
441
+ if (!fs4.existsSync(target)) {
442
+ const clone = await runCommand3("git", ["clone", source, target]);
443
+ if (clone.code !== 0) {
444
+ fail(report, name, `git clone failed: ${clone.stderr.trim() || `exit code ${clone.code}`}`);
445
+ return;
446
+ }
447
+ const checkout = await runCommand3("git", ["-C", target, "checkout", ref]);
448
+ if (checkout.code !== 0) {
449
+ fail(report, name, `git checkout ${ref} failed: ${checkout.stderr.trim() || `exit code ${checkout.code}`}`);
450
+ return;
451
+ }
452
+ report.entries.push({ name, status: "installed", detail: ref });
453
+ return;
454
+ }
455
+ const status = await runCommand3("git", ["-C", target, "status", "--porcelain"]);
456
+ if (status.code !== 0) {
457
+ fail(report, name, `git status failed (not a git repository?): ${status.stderr.trim() || `exit code ${status.code}`}`);
458
+ return;
459
+ }
460
+ if (status.stdout.trim() !== "") {
461
+ const detail = "working tree is dirty; skipping update (commit or stash local changes first)";
462
+ report.entries.push({ name, status: "skipped-dirty", detail });
463
+ report.warnings.push(`${name}: ${detail}`);
464
+ return;
465
+ }
466
+ const head = await runCommand3("git", ["-C", target, "rev-parse", "HEAD"]);
467
+ if (head.code !== 0) {
468
+ fail(report, name, `git rev-parse failed: ${head.stderr.trim() || `exit code ${head.code}`}`);
469
+ return;
470
+ }
471
+ if (head.stdout.trim() === ref) {
472
+ report.entries.push({ name, status: "current", detail: ref });
473
+ return;
474
+ }
475
+ const advertised = await isPinAdvertised(source, ref, runCommand3);
476
+ if (advertised) {
477
+ const fetch2 = await runCommand3("git", ["-C", target, "fetch", "origin", ref]);
478
+ if (fetch2.code === 0 && await checkoutFetchHead(name, target, runCommand3, report)) {
479
+ return;
480
+ }
481
+ if (fetch2.code !== 0) {
482
+ fail(report, name, `git fetch origin ${ref} failed: ${fetch2.stderr.trim() || `exit code ${fetch2.code}`}`);
483
+ return;
484
+ }
485
+ return;
486
+ }
487
+ const fetch = await runCommand3("git", ["-C", target, "fetch", "origin", ref]);
488
+ if (fetch.code === 0) {
489
+ await checkoutFetchHead(name, target, runCommand3, report);
490
+ return;
491
+ }
492
+ if (FULL_SHA.test(ref)) {
493
+ const fetchAll = await runCommand3("git", ["-C", target, "fetch", "origin"]);
494
+ if (fetchAll.code !== 0) {
495
+ fail(report, name, `git fetch origin failed: ${fetchAll.stderr.trim() || `exit code ${fetchAll.code}`}`);
496
+ return;
497
+ }
498
+ const checkout = await runCommand3("git", ["-C", target, "checkout", ref]);
499
+ if (checkout.code !== 0) {
500
+ fail(report, name, `git checkout ${ref} failed: ${checkout.stderr.trim() || `exit code ${checkout.code}`}`);
501
+ return;
502
+ }
503
+ report.entries.push({ name, status: "updated", detail: ref });
504
+ return;
505
+ }
506
+ fail(report, name, `git fetch origin ${ref} failed: ${fetch.stderr.trim() || `exit code ${fetch.code}`}`);
507
+ }
508
+ async function isPinAdvertised(source, ref, runCommand3) {
509
+ const remote = await runCommand3("git", ["ls-remote", source]);
510
+ if (remote.code !== 0) {
511
+ return false;
512
+ }
513
+ return remote.stdout.split("\n").some((line) => line.split(" ")[0]?.trim() === ref);
514
+ }
515
+ async function checkoutFetchHead(name, target, runCommand3, report) {
516
+ const checkout = await runCommand3("git", ["-C", target, "checkout", "FETCH_HEAD"]);
517
+ if (checkout.code !== 0) {
518
+ fail(report, name, `git checkout FETCH_HEAD failed: ${checkout.stderr.trim() || `exit code ${checkout.code}`}`);
519
+ return false;
520
+ }
521
+ report.entries.push({ name, status: "updated", detail: "checked out FETCH_HEAD" });
522
+ return true;
523
+ }
524
+ async function syncCli(name, cfg, options, platform, runCommand3, report) {
525
+ const binary = cfg.binary ?? name;
526
+ const installCommand = cfg.platforms?.[platform] ?? cfg.platforms?.linux;
527
+ const npmPackage = installCommand ? parseNpmGlobalInstall(installCommand) : void 0;
528
+ const pinned = cfg.ref ? pinnedSemver(cfg.ref) : void 0;
529
+ if (options.dryRun) {
530
+ const managed = npmPackage && pinned ? `install ${npmPackage}@${pinned} if missing or older` : "verify presence";
531
+ report.entries.push({ name, status: "dry-run", detail: `would ${managed}` });
532
+ return;
533
+ }
534
+ const whichCmd = process.platform === "win32" ? "where" : "which";
535
+ const found = await runCommand3(whichCmd, [binary]);
536
+ if (found.code !== 0) {
537
+ if (npmPackage && pinned) {
538
+ await npmInstall(name, npmPackage, pinned, runCommand3, report, "installed");
539
+ return;
540
+ }
541
+ fail(report, name, `CLI '${binary}' not found. Install manually (${platform}): ${installCommand ?? "see dependency manifest"}`);
542
+ return;
543
+ }
544
+ if (!npmPackage || !pinned) {
545
+ report.entries.push({ name, status: "current", detail: `${binary} present` });
546
+ return;
547
+ }
548
+ const version = await runCommand3(binary, ["--version"]);
549
+ const installed = version.code === 0 ? version.stdout.split("\n")[0]?.trim() ?? "" : "";
550
+ const comparison = installed ? compareVersions(installed, pinned) : void 0;
551
+ if (comparison === void 0) {
552
+ report.entries.push({
553
+ name,
554
+ status: "current",
555
+ detail: `could not parse installed version '${installed}'; leaving ${binary} as-is`
556
+ });
557
+ return;
558
+ }
559
+ if (comparison >= 0) {
560
+ report.entries.push({ name, status: "current", detail: `${binary} ${installed}` });
561
+ return;
562
+ }
563
+ await npmInstall(name, npmPackage, pinned, runCommand3, report, "updated");
564
+ }
565
+ async function npmInstall(name, npmPackage, version, runCommand3, report, status) {
566
+ const result = await runCommand3("npm", ["install", "-g", `${npmPackage}@${version}`]);
567
+ if (result.code !== 0) {
568
+ fail(report, name, `npm install -g ${npmPackage}@${version} failed: ${result.stderr.trim() || `exit code ${result.code}`}`);
569
+ return;
570
+ }
571
+ report.entries.push({ name, status, detail: `${npmPackage}@${version}` });
572
+ }
573
+ function parseNpmGlobalInstall(command) {
574
+ const match = NPM_GLOBAL_INSTALL.exec(command.trim());
575
+ return match?.[1];
576
+ }
577
+ function pinnedSemver(ref) {
578
+ const match = PINNED_SEMVER.exec(ref.trim());
579
+ return match?.[0].replace(/^v/, "");
580
+ }
581
+ function fail(report, name, detail) {
582
+ report.entries.push({ name, status: "failed", detail });
583
+ report.warnings.push(`${name}: ${detail}`);
584
+ }
585
+
586
+ // src/lib/harnessDetect.ts
587
+ import fs5 from "fs";
588
+ import path6 from "path";
333
589
  import os2 from "os";
334
590
  var HARNESS_SPECS = [
335
591
  {
@@ -343,9 +599,9 @@ var HARNESS_SPECS = [
343
599
  function detectGlobalHarnesses(overrideHomeDir) {
344
600
  const homeDir = overrideHomeDir ?? os2.homedir();
345
601
  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;
602
+ const configDir = path6.join(homeDir, spec.configDirName);
603
+ const installed = fs5.existsSync(configDir);
604
+ const pluginManaged = spec.pluginMarker ? fs5.existsSync(path6.join(configDir, spec.pluginMarker)) : false;
349
605
  return {
350
606
  id: spec.id,
351
607
  installed,
@@ -537,6 +793,7 @@ function createDefaultDeps() {
537
793
  paths: resolveLincolnPaths(),
538
794
  payloadRoot: resolvePayloadRoot() || "",
539
795
  syncHarnesses,
796
+ syncExternalSkills,
540
797
  createPrompt,
541
798
  resolvePythonForVenv,
542
799
  isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY)
@@ -549,7 +806,7 @@ async function install(options, deps = createDefaultDeps()) {
549
806
  return 1;
550
807
  }
551
808
  const payloadRoot = deps.payloadRoot;
552
- if (!payloadRoot || !fs5.existsSync(payloadRoot)) {
809
+ if (!payloadRoot || !fs6.existsSync(payloadRoot)) {
553
810
  console.error('Lincoln payload not found. Run "npm install -g @sushanglewis/lincoln" first.');
554
811
  return 1;
555
812
  }
@@ -568,15 +825,15 @@ async function install(options, deps = createDefaultDeps()) {
568
825
  return 1;
569
826
  }
570
827
  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 });
828
+ const versionDir = path7.join(deps.paths.versionsDir, version);
829
+ fs6.mkdirSync(versionDir, { recursive: true });
830
+ fs6.cpSync(payloadRoot, versionDir, { recursive: true, force: true });
574
831
  try {
575
- fs5.rmSync(deps.paths.currentDir, { recursive: true, force: true });
576
- fs5.symlinkSync(versionDir, deps.paths.currentDir);
832
+ fs6.rmSync(deps.paths.currentDir, { recursive: true, force: true });
833
+ fs6.symlinkSync(versionDir, deps.paths.currentDir);
577
834
  } catch {
578
- fs5.rmSync(deps.paths.currentDir, { recursive: true, force: true });
579
- fs5.cpSync(versionDir, deps.paths.currentDir, { recursive: true, force: true });
835
+ fs6.rmSync(deps.paths.currentDir, { recursive: true, force: true });
836
+ fs6.cpSync(versionDir, deps.paths.currentDir, { recursive: true, force: true });
580
837
  }
581
838
  }
582
839
  const reports = await deps.syncHarnesses({
@@ -596,9 +853,30 @@ async function install(options, deps = createDefaultDeps()) {
596
853
  managedFiles: collectManagedFiles(reports)
597
854
  });
598
855
  }
599
- printSummary(options, version, harnesses, reports);
856
+ const skillReport = await syncSkillLibraries(options, deps, harnesses);
857
+ printSummary(options, version, harnesses, reports, skillReport);
600
858
  return 0;
601
859
  }
860
+ async function syncSkillLibraries(options, deps, harnesses) {
861
+ if (!harnesses.includes("claude-code") || !deps.syncExternalSkills) {
862
+ return void 0;
863
+ }
864
+ const payloadRoot = deps.paths.currentDir;
865
+ const manifestPath = path7.join(payloadRoot, ".claude", "skills", "dependencies.yaml");
866
+ if (!fs6.existsSync(manifestPath)) {
867
+ return void 0;
868
+ }
869
+ try {
870
+ return await deps.syncExternalSkills({
871
+ payloadRoot,
872
+ skillsDir: path7.join(deps.paths.claudeDir, "skills"),
873
+ dryRun: options.dryRun
874
+ });
875
+ } catch (err) {
876
+ console.warn(` warning: external skill sync failed: ${err instanceof Error ? err.message : String(err)}`);
877
+ return void 0;
878
+ }
879
+ }
602
880
  async function resolvePythonIfNeeded(options, deps, harnesses) {
603
881
  const needsPython = !options.dryRun && !options.noVenv && harnesses.some((id) => id === "codex" || id === "opencode");
604
882
  if (!needsPython) {
@@ -659,7 +937,7 @@ function collectManagedFiles(reports) {
659
937
  }
660
938
  return files;
661
939
  }
662
- function printSummary(options, version, harnesses, reports) {
940
+ function printSummary(options, version, harnesses, reports, skillReport) {
663
941
  const warnings = collectWarnings(reports);
664
942
  if (options.dryRun) {
665
943
  console.log(`Dry run: would install Lincoln ${version} for harnesses: ${harnesses.join(", ")}`);
@@ -670,6 +948,13 @@ function printSummary(options, version, harnesses, reports) {
670
948
  const count = report.written.length + report.skipped.length + report.preserved.length;
671
949
  console.log(` ${harnessId}: ${count} file(s)`);
672
950
  }
951
+ if (skillReport) {
952
+ for (const entry of skillReport.entries) {
953
+ const detail = entry.detail ? ` (${entry.detail})` : "";
954
+ console.log(` skill ${entry.name}: ${entry.status}${detail}`);
955
+ }
956
+ warnings.push(...skillReport.warnings);
957
+ }
673
958
  for (const warning of warnings) {
674
959
  console.warn(` warning: ${warning}`);
675
960
  }
@@ -730,7 +1015,7 @@ async function resolvePythonForVenv(envOverride = process.env.LINCOLN_PYTHON, ca
730
1015
  return void 0;
731
1016
  }
732
1017
  function venvPythonPath(paths) {
733
- return path6.join(
1018
+ return path7.join(
734
1019
  paths.venvDir,
735
1020
  process.platform === "win32" ? "Scripts" : "bin",
736
1021
  process.platform === "win32" ? "python.exe" : "python"
@@ -738,17 +1023,17 @@ function venvPythonPath(paths) {
738
1023
  }
739
1024
  async function setupVenv(paths, payloadRoot, pythonCommand) {
740
1025
  const pythonExe = venvPythonPath(paths);
741
- if (!fs5.existsSync(pythonExe)) {
1026
+ if (!fs6.existsSync(pythonExe)) {
742
1027
  console.log("Creating Lincoln virtual environment...");
743
1028
  const venvCode = await runCommand2(pythonCommand, ["-m", "venv", paths.venvDir]);
744
1029
  if (venvCode !== 0) {
745
1030
  throw new Error(`Failed to create virtual environment (exit code ${venvCode})`);
746
1031
  }
747
1032
  }
748
- const requirementsPath = path6.join(payloadRoot, "requirements.txt");
749
- if (fs5.existsSync(requirementsPath)) {
1033
+ const requirementsPath = path7.join(payloadRoot, "requirements.txt");
1034
+ if (fs6.existsSync(requirementsPath)) {
750
1035
  console.log("Installing Python dependencies...");
751
- const pipExe = path6.join(
1036
+ const pipExe = path7.join(
752
1037
  paths.venvDir,
753
1038
  process.platform === "win32" ? "Scripts" : "bin",
754
1039
  process.platform === "win32" ? "pip.exe" : "pip"