pointer-feedback 0.1.2 → 0.1.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.
Files changed (2) hide show
  1. package/dist/cli.js +192 -61
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ var init_build_constants = __esm({
15
15
  "src/build-constants.ts"() {
16
16
  "use strict";
17
17
  BUILD_DEFAULT_SERVER = true ? "https://api.pointer.moamen.work" : "https://api.pointer.moamen.work";
18
- BUILD_CLI_VERSION = true ? "0.1.2" : "0.0.0-dev";
18
+ BUILD_CLI_VERSION = true ? "0.1.3" : "0.0.0-dev";
19
19
  }
20
20
  });
21
21
 
@@ -866,27 +866,62 @@ var init_deployed = __esm({
866
866
 
867
867
  // src/prompt.ts
868
868
  import * as readline from "node:readline/promises";
869
+ import { emitKeypressEvents } from "node:readline";
869
870
  import { Writable } from "node:stream";
871
+ function assertInteractive() {
872
+ if (process.stdin.isTTY)
873
+ return;
874
+ console.error(
875
+ "\x1B[31mThis command is interactive, but stdin is not a terminal.\x1B[0m\nPiped input, CI, and some editor-embedded shells have no TTY, so there is no way to ask you anything.\n\nEither run it in a real terminal, or pass every answer as a flag:\n npx -y pointer-feedback init --server <url> --key ptr_... --project <key> --environment local --yes\n\nRun 'npx -y pointer-feedback init --help' for the full list of flags."
876
+ );
877
+ process.exit(2);
878
+ }
879
+ var muted = false;
880
+ var gatedStdout = new Writable({
881
+ write(chunk, encoding, callback) {
882
+ if (!muted)
883
+ process.stdout.write(chunk, encoding);
884
+ callback();
885
+ }
886
+ });
887
+ var shared = null;
888
+ function iface() {
889
+ if (!shared) {
890
+ shared = readline.createInterface({
891
+ input: process.stdin,
892
+ output: gatedStdout,
893
+ terminal: true
894
+ });
895
+ shared.on("SIGINT", () => {
896
+ process.stdout.write("\n");
897
+ process.exit(130);
898
+ });
899
+ }
900
+ return shared;
901
+ }
902
+ function closePrompts() {
903
+ shared?.close();
904
+ shared = null;
905
+ }
870
906
  async function ask(question, options = {}) {
871
- let muted = false;
872
- const mutableStdout = new Writable({
873
- write: function(chunk, encoding, callback) {
874
- if (!muted)
875
- process.stdout.write(chunk, encoding);
876
- callback();
877
- }
878
- });
879
- const rl = readline.createInterface({
880
- input: process.stdin,
881
- output: mutableStdout,
882
- terminal: true
883
- });
907
+ assertInteractive();
908
+ const rl = iface();
884
909
  const displayQuestion = options.default ? `${question} [${options.default}]: ` : `${question}: `;
885
910
  while (true) {
886
- process.stdout.write(displayQuestion);
911
+ const pending = rl.question(displayQuestion);
887
912
  if (options.secret)
888
913
  muted = true;
889
- const answer = await rl.question("");
914
+ let answer;
915
+ try {
916
+ answer = await pending;
917
+ } catch (err) {
918
+ muted = false;
919
+ if (err?.code === "ABORT_ERR") {
920
+ process.stdout.write("\nCancelled \u2014 nothing was written.\n");
921
+ process.exit(130);
922
+ }
923
+ throw err;
924
+ }
890
925
  muted = false;
891
926
  if (options.secret)
892
927
  process.stdout.write("\n");
@@ -898,35 +933,96 @@ async function ask(question, options = {}) {
898
933
  continue;
899
934
  }
900
935
  }
901
- rl.close();
902
936
  return finalAnswer;
903
937
  }
904
938
  }
939
+ async function menu(question, items, cursorStart, opts) {
940
+ assertInteractive();
941
+ const rl = iface();
942
+ const selected = opts.selected ?? /* @__PURE__ */ new Set();
943
+ let cursor = Math.max(0, Math.min(cursorStart, items.length - 1));
944
+ const hint = opts.hint ?? (opts.multi ? "\x1B[2m \u2191/\u2193 move \xB7 space toggle \xB7 a all \xB7 enter confirm\x1B[0m" : "\x1B[2m \u2191/\u2193 move \xB7 enter select\x1B[0m");
945
+ const render = (first) => {
946
+ if (!first)
947
+ process.stdout.write(`\x1B[${items.length + 1}A`);
948
+ process.stdout.write("\x1B[0J");
949
+ process.stdout.write(`${hint}
950
+ `);
951
+ items.forEach((item, i) => {
952
+ const pointer = i === cursor ? "\x1B[36m\u276F\x1B[0m" : " ";
953
+ const box = opts.multi ? selected.has(i) ? "\x1B[36m[x]\x1B[0m " : "[ ] " : "";
954
+ const label = i === cursor ? `\x1B[36m${item}\x1B[0m` : item;
955
+ process.stdout.write(`${pointer} ${box}${label}
956
+ `);
957
+ });
958
+ };
959
+ console.log(question);
960
+ rl.pause();
961
+ emitKeypressEvents(process.stdin);
962
+ const wasRaw = process.stdin.isRaw ?? false;
963
+ if (process.stdin.setRawMode)
964
+ process.stdin.setRawMode(true);
965
+ process.stdin.resume();
966
+ render(true);
967
+ try {
968
+ return await new Promise((resolve4) => {
969
+ const onKey = (_str, key) => {
970
+ if (key.ctrl && key.name === "c") {
971
+ cleanup();
972
+ process.stdout.write("\n");
973
+ process.exit(130);
974
+ }
975
+ if (key.name === "up" || key.name === "k") {
976
+ cursor = (cursor - 1 + items.length) % items.length;
977
+ render(false);
978
+ } else if (key.name === "down" || key.name === "j") {
979
+ cursor = (cursor + 1) % items.length;
980
+ render(false);
981
+ } else if (opts.multi && (key.name === "space" || key.sequence === " ")) {
982
+ selected.has(cursor) ? selected.delete(cursor) : selected.add(cursor);
983
+ render(false);
984
+ } else if (opts.multi && key.name === "a") {
985
+ if (selected.size === items.length)
986
+ selected.clear();
987
+ else
988
+ items.forEach((_, i) => selected.add(i));
989
+ render(false);
990
+ } else if (key.name === "return" || key.name === "enter") {
991
+ if (opts.multi && selected.size === 0) {
992
+ selected.add(cursor);
993
+ }
994
+ cleanup();
995
+ resolve4(opts.multi ? [...selected].sort((a, b) => a - b) : [cursor]);
996
+ }
997
+ };
998
+ const cleanup = () => {
999
+ process.stdin.off("keypress", onKey);
1000
+ if (process.stdin.setRawMode)
1001
+ process.stdin.setRawMode(wasRaw);
1002
+ };
1003
+ process.stdin.on("keypress", onKey);
1004
+ });
1005
+ } finally {
1006
+ if (process.stdin.setRawMode)
1007
+ process.stdin.setRawMode(wasRaw);
1008
+ rl.resume();
1009
+ }
1010
+ }
905
1011
  async function select(question, items, defaultItem) {
906
- const rl = readline.createInterface({
907
- input: process.stdin,
908
- output: process.stdout,
909
- terminal: true
910
- });
911
- const defaultLabel = defaultItem ? ` [${defaultItem}]` : "";
912
- console.log(`${question}${defaultLabel}`);
913
- items.forEach((item, i) => {
914
- console.log(` ${i + 1}) ${item}`);
1012
+ const start = defaultItem ? Math.max(0, items.indexOf(defaultItem)) : 0;
1013
+ const [chosen] = await menu(question, items, start, {});
1014
+ return items[chosen];
1015
+ }
1016
+ async function multiSelect(question, items, defaults = []) {
1017
+ const selected = /* @__PURE__ */ new Set();
1018
+ defaults.forEach((d) => {
1019
+ const i = items.indexOf(d);
1020
+ if (i >= 0)
1021
+ selected.add(i);
915
1022
  });
916
- while (true) {
917
- const answer = await rl.question("> ");
918
- const finalAnswer = answer.trim() || defaultItem || items[0];
919
- const asNum = parseInt(finalAnswer, 10);
920
- if (!isNaN(asNum) && asNum >= 1 && asNum <= items.length) {
921
- rl.close();
922
- return items[asNum - 1];
923
- }
924
- if (items.includes(finalAnswer)) {
925
- rl.close();
926
- return finalAnswer;
927
- }
928
- console.log("\x1B[31mInvalid selection\x1B[0m");
929
- }
1023
+ const start = selected.size ? Math.min(...selected) : 0;
1024
+ const chosen = await menu(question, items, start, { selected, multi: true });
1025
+ return chosen.map((i) => items[i]);
930
1026
  }
931
1027
 
932
1028
  // src/commands/init.ts
@@ -1522,7 +1618,7 @@ async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
1522
1618
  checks.push({ id: "project", status: "warn", message: `Could not list projects: ${err?.message ?? err}` });
1523
1619
  }
1524
1620
  }
1525
- checks.push(await widgetCheck(cwd2));
1621
+ checks.push(await widgetCheck(cwd2, config));
1526
1622
  if (serverReachable) {
1527
1623
  try {
1528
1624
  const res = await fetchWithTimeout(`${server}/pointer.js`, 3e3);
@@ -1565,9 +1661,9 @@ async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
1565
1661
  }
1566
1662
  return checks;
1567
1663
  }
1568
- async function widgetCheck(cwd2) {
1664
+ async function widgetCheck(cwd2, config = {}) {
1569
1665
  const detection = await detectStack(cwd2).catch(() => null);
1570
- const candidates = [detection?.htmlPath, "index.html", "public/index.html", "src/index.html"].filter(Boolean);
1666
+ const candidates = [config.htmlPath, detection?.htmlPath, "index.html", "public/index.html", "src/index.html"].filter(Boolean);
1571
1667
  for (const rel of candidates) {
1572
1668
  try {
1573
1669
  const html = await fs7.readFile(join7(cwd2, rel), "utf8");
@@ -2415,6 +2511,20 @@ async function initCommand(cwd2, options = {}) {
2415
2511
  }
2416
2512
  await writeCredentials(cwd2, key);
2417
2513
  await upsertGitignore(cwd2, product);
2514
+ const appInfo = await detectStack(cwd2);
2515
+ const canInject = appInfo.kind === "vite" || appInfo.kind === "static" || !!options["html"];
2516
+ if (!isJson && !isYes) {
2517
+ console.log(`
2518
+ Stack: ${appInfo.kind}${appInfo.evidence.length ? ` (${appInfo.evidence.join(", ")})` : ""}`);
2519
+ if (!canInject && !options["no-inject"]) {
2520
+ console.log(
2521
+ `\x1B[33mHeads up:\x1B[0m automatic widget injection isn't supported for ${appInfo.kind} yet.
2522
+ Everything else still applies \u2014 the questions below set up your project, key and skills,
2523
+ and the pointer-init skill uses them to mount the widget for you afterwards.
2524
+ `
2525
+ );
2526
+ }
2527
+ }
2418
2528
  let project = options["project"];
2419
2529
  let create = options["create"];
2420
2530
  let finalProjectKey = project || "";
@@ -2474,24 +2584,16 @@ async function initCommand(cwd2, options = {}) {
2474
2584
  if (!isYes && !options["environment"]) {
2475
2585
  env = await select("Environment", ["local", "staging", "production"], env);
2476
2586
  }
2477
- const appInfo = await detectStack(cwd2);
2478
2587
  let appUrl = options["app-url"];
2479
2588
  let noAppUrl = options["no-app-url"];
2480
2589
  let source = "";
2481
2590
  if (!noAppUrl && !appUrl) {
2482
2591
  const detected = await detectAppUrl(cwd2, appInfo.kind, env);
2483
2592
  source = detected.source;
2484
- if (!isYes) {
2485
- const displayDefault = detected.url ? detected.url : "";
2486
- const ans = await ask(`Where does this app run in ${env}?`, { default: displayDefault });
2487
- appUrl = ans || void 0;
2488
- } else {
2489
- appUrl = detected.url || void 0;
2490
- }
2491
- }
2492
- if (appUrl && env !== "local") {
2593
+ appUrl = detected.url || void 0;
2493
2594
  }
2494
2595
  let tool = options["tool"];
2596
+ let tools = tool ? [tool] : [];
2495
2597
  if (!tool) {
2496
2598
  if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT)
2497
2599
  tool = "claude-code";
@@ -2506,9 +2608,20 @@ async function initCommand(cwd2, options = {}) {
2506
2608
  else
2507
2609
  tool = isYes ? "other" : "claude-code";
2508
2610
  if (!isYes && !options["tool"]) {
2509
- tool = await select("AI tool", ["claude-code", "cursor", "windsurf", "opencode", "antigravity", "other"], tool);
2611
+ const ALL = "all of them";
2612
+ const catalogue = ["claude-code", "cursor", "windsurf", "opencode", "antigravity", "other"];
2613
+ const picked = await multiSelect(
2614
+ "Which AI tools work in this repo?",
2615
+ [ALL, ...catalogue],
2616
+ [tool]
2617
+ );
2618
+ tools = picked.includes(ALL) ? catalogue : picked;
2619
+ tool = tools[0] ?? tool;
2510
2620
  }
2511
2621
  }
2622
+ if (tools.length === 0)
2623
+ tools = [tool];
2624
+ closePrompts();
2512
2625
  if (!isJson)
2513
2626
  console.log(`Detecting your stack... -> ${appInfo.kind} (${appInfo.evidence.join(", ")})`);
2514
2627
  let injected = false;
@@ -2533,7 +2646,19 @@ async function initCommand(cwd2, options = {}) {
2533
2646
  }
2534
2647
  }
2535
2648
  if (!options["no-inject"]) {
2536
- if (appInfo.kind === "vite") {
2649
+ const explicitHtml = options["html"];
2650
+ if (explicitHtml && appInfo.kind !== "vite") {
2651
+ const htmlPath = await injectStatic(cwd2, explicitHtml, {
2652
+ server,
2653
+ key: finalProjectKey,
2654
+ environment: env,
2655
+ pin
2656
+ });
2657
+ filesMod = [htmlPath];
2658
+ injected = true;
2659
+ if (!isJson)
2660
+ console.log(`Injected widget into ${htmlPath}`);
2661
+ } else if (appInfo.kind === "vite") {
2537
2662
  filesMod = await injectVite(cwd2, { server, key: finalProjectKey, environment: env, pin }, options["html"]);
2538
2663
  injected = true;
2539
2664
  if (!isJson)
@@ -2547,18 +2672,23 @@ async function initCommand(cwd2, options = {}) {
2547
2672
  } else if (appInfo.kind !== "unknown") {
2548
2673
  routedToSkill = true;
2549
2674
  if (!isJson) {
2550
- console.log(`\u2139 ${appInfo.kind} detected \u2014 automatic injection isn't supported for this stack yet.
2551
- The pointer-init skill was installed for ${tool}. Run it and it will mount the widget for you:
2675
+ console.log(`\u2139 ${appInfo.kind} detected \u2014 there's no single entry point to inject into automatically.
2676
+ If you know the file, name it and re-run \u2014 that always wins over detection:
2677
+ npx -y pointer-feedback init --html path/to/index.html
2678
+ Otherwise the pointer-init skill was installed for ${tool}; run it and it will mount the widget:
2552
2679
  claude -> /pointer-init (or @pointer-init for cursor)
2553
- Config is already saved in .pointer/config.json, so the skill won't ask for the key or project again.`);
2680
+ Config is already saved in .pointer/config.json, so neither will ask for the key or project again.`);
2554
2681
  }
2555
2682
  }
2556
2683
  }
2557
2684
  if (!options["no-skills"]) {
2558
2685
  if (!isJson)
2559
- console.log("Installing AI skills");
2686
+ console.log(`Installing AI skills for ${tools.join(", ")}`);
2560
2687
  const skillsDir = options["skills-dir"];
2561
- const installed = await installSkills(server, tool, cwd2, skillsDir);
2688
+ const installed = [];
2689
+ for (const t of tools) {
2690
+ installed.push(...await installSkills(server, t, cwd2, t === tool ? skillsDir : void 0));
2691
+ }
2562
2692
  filesMod.push(...installed);
2563
2693
  skillFiles = installed.filter((f) => f.includes("SKILL.md") || f.endsWith(".md"));
2564
2694
  }
@@ -2584,7 +2714,8 @@ async function initCommand(cwd2, options = {}) {
2584
2714
  }
2585
2715
  const mergedStack = mergeStack(stackMeta, serverStackResponse?.data ?? serverStackResponse, noDesign ? null : designBlock);
2586
2716
  await writeStackFile(cwd2, mergedStack);
2587
- await writeConfig(cwd2, { server, project: finalProjectKey, environment: env, aiTool: tool, skillsDir: options["skills-dir"], cliVersion: BUILD_CLI_VERSION });
2717
+ const injectedHtml = injected ? filesMod.find((f) => f.toLowerCase().endsWith(".html"))?.replace(`${cwd2}/`, "") : void 0;
2718
+ await writeConfig(cwd2, { server, project: finalProjectKey, environment: env, aiTool: tool, skillsDir: options["skills-dir"], cliVersion: BUILD_CLI_VERSION, htmlPath: injectedHtml });
2588
2719
  filesMod.push(".pointer/config.json");
2589
2720
  filesMod.push(".pointer/credentials.env");
2590
2721
  filesMod.push(".gitignore");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pointer-feedback",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Click-to-comment feedback for your web app: install the widget, then turn pending comments into AI apply prompts.",
5
5
  "type": "module",
6
6
  "license": "MIT",