@bilalimamoglu/sift 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +215 -42
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -24,6 +24,9 @@ function getDefaultCodexGlobalSkillPath(homeDir = os.homedir()) {
24
24
  function getDefaultCursorGlobalSkillPath(homeDir = os.homedir()) {
25
25
  return path.join(homeDir, ".cursor", "skills", "sift", "SKILL.md");
26
26
  }
27
+ function getDefaultCopilotInstructionsPath(cwd = process.cwd()) {
28
+ return path.join(cwd, ".github", "copilot-instructions.md");
29
+ }
27
30
  function getDefaultClaudeGlobalInstructionsPath(homeDir = os.homedir()) {
28
31
  return path.join(homeDir, ".claude", "CLAUDE.md");
29
32
  }
@@ -762,6 +765,7 @@ function describeInsufficientBehavior(mode) {
762
765
  import { execFileSync } from "child_process";
763
766
  import { clearScreenDown, cursorTo, moveCursor } from "readline";
764
767
  import { stdin as defaultStdin } from "process";
768
+ import stripAnsi from "strip-ansi";
765
769
  var PROMPT_BACK = "__sift_back__";
766
770
  var PROMPT_BACK_LABEL = "\u2190 Back";
767
771
  function color(text, rgb, args = {}) {
@@ -849,6 +853,16 @@ function styleOption(option, selected, colorize) {
849
853
  dim: !selected && Boolean(palette.dimWhenIdle)
850
854
  })}${trailing}`;
851
855
  }
856
+ function getTerminalColumns(stream) {
857
+ return typeof stream.columns === "number" && stream.columns > 0 ? stream.columns : 80;
858
+ }
859
+ function getVisualLineCount(line, columns) {
860
+ const visibleLength = stripAnsi(line).length;
861
+ return Math.max(1, Math.ceil(visibleLength / columns));
862
+ }
863
+ function getVisualBlockHeight(lines, columns) {
864
+ return lines.reduce((total, line) => total + getVisualLineCount(line, columns), 0);
865
+ }
852
866
  function setPosixEcho(enabled) {
853
867
  const command = enabled ? "echo" : "-echo";
854
868
  try {
@@ -902,7 +916,7 @@ async function promptSelect(args) {
902
916
  });
903
917
  output.write(`${lines.join("\n")}
904
918
  `);
905
- previousLineCount = lines.length;
919
+ previousLineCount = getVisualBlockHeight(lines, getTerminalColumns(stream));
906
920
  };
907
921
  const cleanup = (selected) => {
908
922
  if (previousLineCount > 0) {
@@ -9629,11 +9643,11 @@ function buildSafetyStderrNotice(report) {
9629
9643
  }
9630
9644
 
9631
9645
  // src/core/sanitize.ts
9632
- import stripAnsi from "strip-ansi";
9646
+ import stripAnsi2 from "strip-ansi";
9633
9647
  function sanitizeInput(input, stripAnsiEnabled) {
9634
9648
  let output = input;
9635
9649
  if (stripAnsiEnabled) {
9636
- output = stripAnsi(output);
9650
+ output = stripAnsi2(output);
9637
9651
  }
9638
9652
  return output.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\u0000/g, "").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
9639
9653
  }
@@ -12262,6 +12276,7 @@ async function runHook(args, deps = defaultHookDeps) {
12262
12276
  }
12263
12277
 
12264
12278
  // src/commands/install.ts
12279
+ import fs9 from "fs";
12265
12280
  import os5 from "os";
12266
12281
  import path10 from "path";
12267
12282
  import { emitKeypressEvents as emitKeypressEvents2 } from "readline";
@@ -12271,10 +12286,42 @@ import {
12271
12286
  stdin as defaultStdin4,
12272
12287
  stdout as defaultStdout3
12273
12288
  } from "process";
12289
+
12290
+ // src/runtime-payloads/copilot-instructions.ts
12291
+ var COPILOT_INSTRUCTIONS_MARKER = "<!-- sift:generated copilot-instructions -->";
12292
+ function renderCopilotInstructions() {
12293
+ return [
12294
+ COPILOT_INSTRUCTIONS_MARKER,
12295
+ "",
12296
+ "# Sift",
12297
+ "",
12298
+ "Use `sift` as the first pass when command output is long, noisy, and non-interactive.",
12299
+ "The goal is to turn failure walls into grouped issues, likely root causes, and the next useful step before reading raw logs.",
12300
+ "",
12301
+ "## Repository Guidance",
12302
+ "",
12303
+ "- Start with `sift exec` before reading raw logs when the output is long or noisy.",
12304
+ "- For test failure walls, prefer `sift exec --preset test-status -- <test command>` first.",
12305
+ "- If `standard` already shows the root cause, stop there and read the relevant source instead of chasing more output.",
12306
+ "- After a fix, use `sift rerun` to refresh the same test suite and confirm what changed.",
12307
+ "- Keep edits focused and update tests when behavior changes.",
12308
+ "- Prefer the existing repo conventions in nearby code before introducing new patterns.",
12309
+ "- Keep generated sift-owned files intact; do not hand-edit them unless you are intentionally changing the generator.",
12310
+ "- Run `npm test` and `npm run typecheck` before handing off code changes.",
12311
+ "",
12312
+ "## Notes",
12313
+ "",
12314
+ "- This repository uses `sift` as the CLI runtime and `copilot-instructions.md` as a repo-wide guidance surface for Copilot.",
12315
+ "- The instructions are intentionally short so they stay useful instead of becoming another wall of text."
12316
+ ].join("\n");
12317
+ }
12318
+
12319
+ // src/commands/install.ts
12274
12320
  var INSTALL_TITLES = {
12275
12321
  codex: "Codex",
12276
12322
  claude: "Claude",
12277
- cursor: "Cursor"
12323
+ cursor: "Cursor",
12324
+ copilot: "Copilot"
12278
12325
  };
12279
12326
  function createInstallTerminalIO() {
12280
12327
  let rl;
@@ -12321,10 +12368,10 @@ function normalizeInstallRuntime(value) {
12321
12368
  if (value === void 0 || value === null || value === "") {
12322
12369
  return void 0;
12323
12370
  }
12324
- if (value === "codex" || value === "claude" || value === "cursor" || value === "all") {
12371
+ if (value === "codex" || value === "claude" || value === "cursor" || value === "copilot" || value === "all") {
12325
12372
  return value;
12326
12373
  }
12327
- throw new Error("Invalid runtime. Use codex, claude, cursor, or all.");
12374
+ throw new Error("Invalid runtime. Use codex, claude, cursor, copilot, or all.");
12328
12375
  }
12329
12376
  function normalizeInstallScope(value) {
12330
12377
  if (value === void 0 || value === null || value === "") {
@@ -12375,15 +12422,43 @@ function getLocalTargetLabel(runtime, cwd = process.cwd()) {
12375
12422
  if (runtime === "claude") {
12376
12423
  return path10.join(cwd, "CLAUDE.md");
12377
12424
  }
12425
+ if (runtime === "copilot") {
12426
+ return getDefaultCopilotInstructionsPath(cwd);
12427
+ }
12378
12428
  return path10.join(cwd, ".cursor", "skills", "sift", "SKILL.md");
12379
12429
  }
12380
12430
  function describeScopeChoice(args) {
12381
12431
  const targets = getInstallTargets(args.runtime);
12382
12432
  const labels = targets.map(
12383
- (agent) => args.scope === "global" ? getGlobalTargetLabel(agent, args.homeDir) : getLocalTargetLabel(agent, args.cwd)
12433
+ (agent) => args.runtime === "copilot" ? getLocalTargetLabel("copilot", args.cwd) : args.scope === "global" ? getGlobalTargetLabel(agent, args.homeDir) : getLocalTargetLabel(agent, args.cwd)
12384
12434
  );
12385
12435
  return labels.join(" + ");
12386
12436
  }
12437
+ function getCopilotInstructionsTargetPath(cwd) {
12438
+ return getDefaultCopilotInstructionsPath(cwd ?? process.cwd());
12439
+ }
12440
+ function inspectCopilotInstructionsOwnership(content) {
12441
+ if (content === void 0) {
12442
+ return "missing";
12443
+ }
12444
+ return content.includes("<!-- sift:generated copilot-instructions -->") ? "managed" : "custom";
12445
+ }
12446
+ function readOptionalFile2(targetPath) {
12447
+ if (!fs9.existsSync(targetPath)) {
12448
+ return void 0;
12449
+ }
12450
+ const stats = fs9.statSync(targetPath);
12451
+ if (!stats.isFile()) {
12452
+ throw new Error(`${targetPath} exists but is not a file.`);
12453
+ }
12454
+ return fs9.readFileSync(targetPath, "utf8");
12455
+ }
12456
+ function writeTextFileAtomic3(targetPath, content) {
12457
+ fs9.mkdirSync(path10.dirname(targetPath), { recursive: true });
12458
+ const tempPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
12459
+ fs9.writeFileSync(tempPath, content, "utf8");
12460
+ fs9.renameSync(tempPath, targetPath);
12461
+ }
12387
12462
  async function promptWithMenu(args) {
12388
12463
  const defaultIndex = args.defaultIndex ?? 0;
12389
12464
  if (args.io.select) {
@@ -12449,6 +12524,10 @@ async function promptForRuntime(io) {
12449
12524
  label: "Cursor (.cursor/skills/sift/SKILL.md / ~/.cursor/skills/sift/SKILL.md) - native skill path for Cursor without inventing a second runtime",
12450
12525
  value: "cursor"
12451
12526
  },
12527
+ {
12528
+ label: "Copilot (.github/copilot-instructions.md) - repository instructions for GitHub Copilot, repo-only by design",
12529
+ value: "copilot"
12530
+ },
12452
12531
  {
12453
12532
  label: "All - Codex + Claude together if you refuse to pick favorites today",
12454
12533
  value: "all"
@@ -12522,12 +12601,6 @@ function writeSuccessSummary(args) {
12522
12601
  const ui = createPresentation(args.io.stdoutIsTTY);
12523
12602
  const targets = getInstallTargets(args.runtime);
12524
12603
  const scopeLabel = args.scope === "global" ? "global" : "local";
12525
- const targetLabel = describeScopeChoice({
12526
- runtime: args.runtime,
12527
- scope: args.scope,
12528
- cwd: args.cwd,
12529
- homeDir: args.homeDir
12530
- });
12531
12604
  if (args.io.stdoutIsTTY) {
12532
12605
  args.io.write(`
12533
12606
  ${ui.success("Installed runtime support.")}
@@ -12535,6 +12608,33 @@ ${ui.success("Installed runtime support.")}
12535
12608
  } else {
12536
12609
  args.io.write("Installed runtime support.\n");
12537
12610
  }
12611
+ if (targets.includes("copilot")) {
12612
+ const targetLabel2 = getCopilotInstructionsTargetPath(args.cwd);
12613
+ args.io.write(`${ui.note("Copilot instructions installed for this repository.")}
12614
+ `);
12615
+ args.io.write(`${ui.note(`Repo-local instructions live at ${targetLabel2}.`)}
12616
+ `);
12617
+ args.io.write(`${ui.note("Copilot is repo-only here; there is no global install target.")}
12618
+ `);
12619
+ args.io.write(`
12620
+ ${ui.section("Try next")}
12621
+ `);
12622
+ args.io.write(` ${ui.command("sift exec --preset test-status -- pytest -q")}${ui.note(" # default first pass")}
12623
+ `);
12624
+ args.io.write(` ${ui.command("sift doctor")}${ui.note(" # verify the setup and see what happens on ambiguous cases")}
12625
+ `);
12626
+ args.io.write(`${ui.note(getDefaultExecPathLine())}
12627
+ `);
12628
+ args.io.write(`${ui.note(getHookBetaLine())}
12629
+ `);
12630
+ return;
12631
+ }
12632
+ const targetLabel = describeScopeChoice({
12633
+ runtime: args.runtime,
12634
+ scope: args.scope,
12635
+ cwd: args.cwd,
12636
+ homeDir: args.homeDir
12637
+ });
12538
12638
  args.io.write(
12539
12639
  `${ui.note(`Runtime instructions installed for ${targets.map((target) => INSTALL_TITLES[target]).join(" + ")} in ${scopeLabel} scope.`)}
12540
12640
  `
@@ -12589,6 +12689,21 @@ ${ui.section("Try next")}
12589
12689
  function writePreflightSummary(args) {
12590
12690
  const ui = createPresentation(args.io.stdoutIsTTY);
12591
12691
  const runtimeTargets = getInstallTargets(args.runtime);
12692
+ if (runtimeTargets.includes("copilot")) {
12693
+ const targetPath = getCopilotInstructionsTargetPath(args.cwd);
12694
+ args.io.write(`
12695
+ ${ui.section("Install preflight")}
12696
+ `);
12697
+ args.io.write(`${ui.note("Will write repo-local instructions for GitHub Copilot:")}
12698
+ `);
12699
+ args.io.write(` ${ui.command(targetPath)}
12700
+ `);
12701
+ args.io.write(`${ui.note("Will not write shell rc files, PATH entries, git hooks, or arbitrary repo files.")}
12702
+ `);
12703
+ args.io.write(`${ui.note("Managed files update only when sift can prove ownership.")}
12704
+ `);
12705
+ return;
12706
+ }
12592
12707
  const writeTargets = runtimeTargets.flatMap((runtime) => {
12593
12708
  if (runtime === "codex") {
12594
12709
  return [
@@ -12636,6 +12751,7 @@ ${ui.section("Install preflight")}
12636
12751
  }
12637
12752
  async function installRuntimeSupport(options) {
12638
12753
  const io = options.io ?? createInstallTerminalIO();
12754
+ const runtimeIsCopilot = options.runtime === "copilot";
12639
12755
  const getPreviousEditableStep = (step) => {
12640
12756
  if (step === "runtime") {
12641
12757
  return void 0;
@@ -12667,9 +12783,13 @@ async function installRuntimeSupport(options) {
12667
12783
  return void 0;
12668
12784
  };
12669
12785
  try {
12670
- if ((!io.stdinIsTTY || !io.stdoutIsTTY) && (!options.runtime || !options.scope || !options.yes)) {
12786
+ if (runtimeIsCopilot && options.scope === "global") {
12787
+ io.error("sift install copilot is repo-only. Use `sift install copilot --scope local` or omit --scope.\n");
12788
+ return 1;
12789
+ }
12790
+ if ((!io.stdinIsTTY || !io.stdoutIsTTY) && (!options.runtime || !runtimeIsCopilot && !options.scope || !options.yes)) {
12671
12791
  io.error(
12672
- "sift install is interactive and requires a TTY. For non-interactive use `sift install codex --scope global --yes`.\n"
12792
+ "sift install is interactive and requires a TTY. For non-interactive use `sift install codex --scope global --yes` or `sift install copilot --yes`.\n"
12673
12793
  );
12674
12794
  return 1;
12675
12795
  }
@@ -12681,17 +12801,24 @@ async function installRuntimeSupport(options) {
12681
12801
  let operationMode = options.operationMode;
12682
12802
  let scope = options.scope;
12683
12803
  let step;
12804
+ if (runtime === "copilot") {
12805
+ scope = "repo";
12806
+ operationMode ??= "agent-escalation";
12807
+ }
12684
12808
  if (!io.stdinIsTTY || !io.stdoutIsTTY) {
12685
12809
  runtime ??= options.runtime;
12686
12810
  operationMode ??= "agent-escalation";
12811
+ if (runtime === "copilot") {
12812
+ scope = "repo";
12813
+ }
12687
12814
  step = void 0;
12688
12815
  } else if (!runtime) {
12689
12816
  step = "runtime";
12690
- } else if (!operationMode) {
12817
+ } else if (runtime !== "copilot" && !operationMode) {
12691
12818
  step = "mode";
12692
- } else if (!scope) {
12819
+ } else if (runtime !== "copilot" && !scope) {
12693
12820
  step = "scope";
12694
- } else if (operationMode === "provider-assisted") {
12821
+ } else if (runtime !== "copilot" && operationMode === "provider-assisted") {
12695
12822
  step = "provider";
12696
12823
  }
12697
12824
  while (step) {
@@ -12704,6 +12831,12 @@ ${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched an
12704
12831
  return 0;
12705
12832
  }
12706
12833
  runtime = runtimeChoice;
12834
+ if (runtime === "copilot") {
12835
+ scope = "repo";
12836
+ operationMode ??= "agent-escalation";
12837
+ step = void 0;
12838
+ continue;
12839
+ }
12707
12840
  step = !operationMode ? "mode" : !scope ? "scope" : operationMode === "provider-assisted" ? "provider" : void 0;
12708
12841
  continue;
12709
12842
  }
@@ -12754,41 +12887,81 @@ ${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched an
12754
12887
  step = operationMode === "provider-assisted" ? "provider" : void 0;
12755
12888
  continue;
12756
12889
  }
12757
- if (scope === "repo") {
12758
- io.write(
12759
- `
12890
+ if (step === "provider") {
12891
+ if (scope === "repo") {
12892
+ io.write(
12893
+ `
12760
12894
  ${createPresentation(io.stdoutIsTTY).note("Local only applies to the runtime instructions in this repo. Provider fallback config is still machine-wide so sift can reuse it anywhere.")}
12761
12895
  `
12762
- );
12763
- }
12764
- io.write(`
12896
+ );
12897
+ }
12898
+ io.write(`
12765
12899
  ${createPresentation(io.stdoutIsTTY).info("Next: provider setup. Press Esc at any step to go back.")}
12766
12900
  `);
12767
- const setupStatus = await configSetup({
12768
- io,
12769
- env: process.env,
12770
- embedded: true,
12771
- forcedMode: "provider-assisted",
12772
- targetPath: getDefaultGlobalConfigPath(options.homeDir)
12773
- });
12774
- if (setupStatus === CONFIG_SETUP_BACK) {
12775
- const previous = getPreviousEditableStep("provider");
12776
- if (!previous) {
12777
- io.write(`
12901
+ const setupStatus = await configSetup({
12902
+ io,
12903
+ env: process.env,
12904
+ embedded: true,
12905
+ forcedMode: "provider-assisted",
12906
+ targetPath: getDefaultGlobalConfigPath(options.homeDir)
12907
+ });
12908
+ if (setupStatus === CONFIG_SETUP_BACK) {
12909
+ const previous = getPreviousEditableStep("provider");
12910
+ if (!previous) {
12911
+ io.write(`
12778
12912
  ${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
12779
12913
  `);
12780
- return 0;
12914
+ return 0;
12915
+ }
12916
+ step = previous;
12917
+ continue;
12918
+ }
12919
+ if (setupStatus !== 0) {
12920
+ return setupStatus;
12781
12921
  }
12782
- step = previous;
12922
+ step = void 0;
12783
12923
  continue;
12784
12924
  }
12785
- if (setupStatus !== 0) {
12786
- return setupStatus;
12787
- }
12788
- step = void 0;
12789
12925
  }
12790
12926
  const nestedIo = createNestedInstallIO(io);
12927
+ if (runtime === "copilot") {
12928
+ const targetPath = getCopilotInstructionsTargetPath(options.cwd);
12929
+ const existingContent = readOptionalFile2(targetPath);
12930
+ const ownership = inspectCopilotInstructionsOwnership(existingContent);
12931
+ if (ownership === "custom") {
12932
+ io.error(
12933
+ `Refusing to overwrite a custom copilot-instructions.md at ${targetPath}. Move it, remove it manually, or choose a different target path.
12934
+ `
12935
+ );
12936
+ return 1;
12937
+ }
12938
+ if (io.stdoutIsTTY) {
12939
+ writePreflightSummary({
12940
+ io,
12941
+ runtime,
12942
+ scope: "repo",
12943
+ operationMode: operationMode ?? "agent-escalation",
12944
+ cwd: options.cwd,
12945
+ homeDir: options.homeDir
12946
+ });
12947
+ }
12948
+ writeTextFileAtomic3(targetPath, `${renderCopilotInstructions()}
12949
+ `);
12950
+ writeSuccessSummary({
12951
+ io,
12952
+ version: options.version,
12953
+ runtime,
12954
+ scope: "repo",
12955
+ operationMode: operationMode ?? "agent-escalation",
12956
+ cwd: options.cwd,
12957
+ homeDir: options.homeDir
12958
+ });
12959
+ return 0;
12960
+ }
12791
12961
  for (const runtimeTarget of getInstallTargets(runtime)) {
12962
+ if (runtimeTarget === "copilot") {
12963
+ continue;
12964
+ }
12792
12965
  const status = runtimeTarget === "cursor" ? await installSkill({
12793
12966
  runtime: "cursor",
12794
12967
  scope,
@@ -13881,7 +14054,7 @@ function createCliApp(args = {}) {
13881
14054
  stdout.write(`${output}
13882
14055
  `);
13883
14056
  });
13884
- cli.command("install [runtime]", "Interactive runtime installer for Codex, Claude, and tiny Cursor packaging").usage("install [runtime] [options]").example("install").example("install codex").example("install cursor").example("install codex --scope global --yes").example("install all --scope local --yes").option("--scope <scope>", "Install scope: local | global").option("--yes", "Skip prompts when runtime and scope are already provided").action(async (runtime, options) => {
14057
+ cli.command("install [runtime]", "Interactive runtime installer for Codex, Claude, Cursor, and Copilot repo instructions").usage("install [runtime] [options]").example("install").example("install codex").example("install cursor").example("install copilot").example("install codex --scope global --yes").example("install all --scope local --yes").option("--scope <scope>", "Install scope: local | global").option("--yes", "Skip prompts when runtime and scope are already provided").action(async (runtime, options) => {
13885
14058
  process.exitCode = await deps.installRuntimeSupport({
13886
14059
  runtime: normalizeInstallRuntime(runtime),
13887
14060
  scope: normalizeInstallScope(options.scope),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bilalimamoglu/sift",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Local-first output guidance for coding agents working through noisy command output.",
5
5
  "type": "module",
6
6
  "bin": {