pointer-feedback 0.1.2 → 0.1.4

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 +306 -75
  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.4" : "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
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);
910
1022
  });
911
- const defaultLabel = defaultItem ? ` [${defaultItem}]` : "";
912
- console.log(`${question}${defaultLabel}`);
913
- items.forEach((item, i) => {
914
- console.log(` ${i + 1}) ${item}`);
915
- });
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
@@ -1148,6 +1244,8 @@ async function injectStatic(cwd2, htmlPath, cfg) {
1148
1244
  const pinnedProps = cfg.pin ? `
1149
1245
  s.integrity = '${cfg.pin.integrity}';
1150
1246
  s.crossOrigin = 'anonymous';` : "";
1247
+ const pinnedAttrs = cfg.pin ? ` integrity="${cfg.pin.integrity}" crossorigin="anonymous"` : "";
1248
+ const multiEnv = (cfg.environments?.length ?? 0) > 1 || Object.keys(cfg.envMap ?? {}).length > 0;
1151
1249
  const block = cfg.envGuarded ? `<!-- pointer-feedback:start -->
1152
1250
  <script>
1153
1251
  if (
@@ -1158,19 +1256,61 @@ async function injectStatic(cwd2, htmlPath, cfg) {
1158
1256
  s.src = '%VITE_POINTER_SERVER%/pointer.js${pinnedSrc}';${pinnedProps}
1159
1257
  s.defer = true;
1160
1258
  document.head.appendChild(s);
1161
- var el = document.createElement('pointer-feedback');
1162
- el.setAttribute('project', '%VITE_POINTER_PROJECT%');
1163
- el.setAttribute('server', '%VITE_POINTER_SERVER%');
1164
- el.setAttribute('environment', '%VITE_POINTER_ENV%');
1165
- el.setAttribute('source-attr', 'data-component-source');
1166
- document.body.appendChild(el);
1259
+ // Deferred until the body exists. Injected just above </body> this is already true, but the
1260
+ // block gets copied into other files by hand, and inside <head> document.body is null \u2014
1261
+ // "Cannot read properties of null (reading 'appendChild')", and nothing mounts.
1262
+ var mount = function () {
1263
+ if (document.querySelector('pointer-feedback')) return;
1264
+ var el = document.createElement('pointer-feedback');
1265
+ el.setAttribute('project', '%VITE_POINTER_PROJECT%');
1266
+ el.setAttribute('server', '%VITE_POINTER_SERVER%');
1267
+ el.setAttribute('environment', '%VITE_POINTER_ENV%');
1268
+ el.setAttribute('source-attr', 'data-component-source');
1269
+ document.body.appendChild(el);
1270
+ };
1271
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mount);
1272
+ else mount();
1167
1273
  }
1168
1274
  </script>
1169
- <!-- pointer-feedback:end -->` : cfg.pin ? `<!-- pointer-feedback:start -->
1170
- <script src="${cfg.server}/pointer.js?v=${cfg.pin.version}" integrity="${cfg.pin.integrity}" crossorigin="anonymous" defer></script>
1171
- <pointer-feedback project="${cfg.key}" server="${cfg.server}" environment="${cfg.environment}" source-attr="data-component-source"></pointer-feedback>
1172
- <!-- pointer-feedback:end -->` : `<!-- pointer-feedback:start -->
1173
- <script src="${cfg.server}/pointer.js" defer></script>
1275
+ <!-- pointer-feedback:end -->` : multiEnv ? (
1276
+ // One file, several environments.
1277
+ //
1278
+ // The single-environment form writes environment="local" into the markup, which is right
1279
+ // until the same index.html is built for staging and production too — then every comment
1280
+ // from every deployment is tagged `local` and nothing can tell them apart. This form
1281
+ // resolves the environment from the page's own origin at runtime, using the URLs already
1282
+ // registered against the project, so one committed file is correct everywhere.
1283
+ `<!-- pointer-feedback:start -->
1284
+ <script${pinnedAttrs} src="${cfg.server}/pointer.js${pinnedSrc}" defer></script>
1285
+ <script>
1286
+ (function () {
1287
+ var ORIGINS = ${JSON.stringify(cfg.envMap ?? {})};
1288
+ var FALLBACK = '${cfg.environment}';
1289
+ function pointerEnv() {
1290
+ if (ORIGINS[location.origin]) return ORIGINS[location.origin];
1291
+ // A dev server's port changes more often than anyone updates a URL list, so localhost is
1292
+ // recognised by host rather than by exact origin.
1293
+ if (/^(localhost|127\\.0\\.0\\.1|\\[::1\\])$/.test(location.hostname)) return 'local';
1294
+ return FALLBACK;
1295
+ }
1296
+ function mount() {
1297
+ if (document.querySelector('pointer-feedback')) return;
1298
+ var el = document.createElement('pointer-feedback');
1299
+ el.setAttribute('project', '${cfg.key}');
1300
+ el.setAttribute('server', '${cfg.server}');
1301
+ el.setAttribute('environment', pointerEnv());
1302
+ el.setAttribute('source-attr', 'data-component-source');
1303
+ document.body.appendChild(el);
1304
+ }
1305
+ // document.body is null while the parser is still in <head>. Waiting for DOMContentLoaded
1306
+ // makes the snippet work wherever it is pasted, instead of only just above </body>.
1307
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mount);
1308
+ else mount();
1309
+ })();
1310
+ </script>
1311
+ <!-- pointer-feedback:end -->`
1312
+ ) : `<!-- pointer-feedback:start -->
1313
+ <script${pinnedAttrs} src="${cfg.server}/pointer.js${pinnedSrc}" defer></script>
1174
1314
  <pointer-feedback project="${cfg.key}" server="${cfg.server}" environment="${cfg.environment}" source-attr="data-component-source"></pointer-feedback>
1175
1315
  <!-- pointer-feedback:end -->`;
1176
1316
  const re = /<!-- pointer-feedback:start -->[\s\S]*?<!-- pointer-feedback:end -->/;
@@ -1522,7 +1662,7 @@ async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
1522
1662
  checks.push({ id: "project", status: "warn", message: `Could not list projects: ${err?.message ?? err}` });
1523
1663
  }
1524
1664
  }
1525
- checks.push(await widgetCheck(cwd2));
1665
+ checks.push(await widgetCheck(cwd2, config));
1526
1666
  if (serverReachable) {
1527
1667
  try {
1528
1668
  const res = await fetchWithTimeout(`${server}/pointer.js`, 3e3);
@@ -1565,9 +1705,9 @@ async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
1565
1705
  }
1566
1706
  return checks;
1567
1707
  }
1568
- async function widgetCheck(cwd2) {
1708
+ async function widgetCheck(cwd2, config = {}) {
1569
1709
  const detection = await detectStack(cwd2).catch(() => null);
1570
- const candidates = [detection?.htmlPath, "index.html", "public/index.html", "src/index.html"].filter(Boolean);
1710
+ const candidates = [config.htmlPath, detection?.htmlPath, "index.html", "public/index.html", "src/index.html"].filter(Boolean);
1571
1711
  for (const rel of candidates) {
1572
1712
  try {
1573
1713
  const html = await fs7.readFile(join7(cwd2, rel), "utf8");
@@ -2415,6 +2555,20 @@ async function initCommand(cwd2, options = {}) {
2415
2555
  }
2416
2556
  await writeCredentials(cwd2, key);
2417
2557
  await upsertGitignore(cwd2, product);
2558
+ const appInfo = await detectStack(cwd2);
2559
+ const canInject = appInfo.kind === "vite" || appInfo.kind === "static" || !!options["html"];
2560
+ if (!isJson && !isYes) {
2561
+ console.log(`
2562
+ Stack: ${appInfo.kind}${appInfo.evidence.length ? ` (${appInfo.evidence.join(", ")})` : ""}`);
2563
+ if (!canInject && !options["no-inject"]) {
2564
+ console.log(
2565
+ `\x1B[33mHeads up:\x1B[0m automatic widget injection isn't supported for ${appInfo.kind} yet.
2566
+ Everything else still applies \u2014 the questions below set up your project, key and skills,
2567
+ and the pointer-init skill uses them to mount the widget for you afterwards.
2568
+ `
2569
+ );
2570
+ }
2571
+ }
2418
2572
  let project = options["project"];
2419
2573
  let create = options["create"];
2420
2574
  let finalProjectKey = project || "";
@@ -2470,28 +2624,74 @@ async function initCommand(cwd2, options = {}) {
2470
2624
  }
2471
2625
  }
2472
2626
  }
2473
- let env = options["environment"] || "local";
2627
+ const ALL_ENVS = ["local", "staging", "production"];
2628
+ let envs = String(options["environment"] ?? "").split(",").map((e) => e.trim()).filter(Boolean);
2629
+ const badEnv = envs.find((e) => !ALL_ENVS.includes(e));
2630
+ if (badEnv) {
2631
+ console.error(`Unknown environment "${badEnv}". Valid values: ${ALL_ENVS.join(", ")}.`);
2632
+ process.exit(2);
2633
+ }
2474
2634
  if (!isYes && !options["environment"]) {
2475
- env = await select("Environment", ["local", "staging", "production"], env);
2635
+ envs = await multiSelect("Which environments does this codebase run in?", ALL_ENVS, ["local"]);
2636
+ }
2637
+ if (envs.length === 0)
2638
+ envs = ["local"];
2639
+ const env = ALL_ENVS.filter((e) => envs.includes(e))[0] ?? "local";
2640
+ const projectRow = await api(server, "/api/admin/projects", { token }).then((rows) => rows.find((p) => p.key === finalProjectKey)).catch(() => null);
2641
+ if (projectRow?.id) {
2642
+ const activation = {};
2643
+ if (envs.includes("local") && !projectRow.isActiveLocal)
2644
+ activation["isActiveLocal"] = true;
2645
+ if (envs.includes("staging") && !projectRow.isActiveStaging)
2646
+ activation["isActiveStaging"] = true;
2647
+ if (envs.includes("production") && !projectRow.isActiveProduction)
2648
+ activation["isActiveProduction"] = true;
2649
+ if (Object.keys(activation).length) {
2650
+ try {
2651
+ await api(server, `/api/admin/projects/${projectRow.id}`, {
2652
+ method: "PATCH",
2653
+ body: activation,
2654
+ token
2655
+ });
2656
+ } catch (err) {
2657
+ if (!isJson) {
2658
+ console.error(
2659
+ `Note: could not activate ${Object.keys(activation).length} environment(s) for this project (${err?.message ?? err}). An admin can switch them on in the dashboard.`
2660
+ );
2661
+ }
2662
+ }
2663
+ }
2664
+ }
2665
+ const envMap = {};
2666
+ if (projectRow?.id && envs.length > 1) {
2667
+ try {
2668
+ const [urls, environments] = await Promise.all([
2669
+ api(server, `/api/admin/projects/${projectRow.id}/app-urls`, { token }),
2670
+ api(server, "/api/admin/environments", { token })
2671
+ ]);
2672
+ const nameById = new Map((environments ?? []).map((e) => [e.id, String(e.name ?? "").toLowerCase()]));
2673
+ for (const row of urls ?? []) {
2674
+ const name = nameById.get(row.appEnvironmentId);
2675
+ if (!name || !row.url || !envs.includes(name))
2676
+ continue;
2677
+ try {
2678
+ envMap[new URL(row.url).origin] = name;
2679
+ } catch {
2680
+ }
2681
+ }
2682
+ } catch {
2683
+ }
2476
2684
  }
2477
- const appInfo = await detectStack(cwd2);
2478
2685
  let appUrl = options["app-url"];
2479
2686
  let noAppUrl = options["no-app-url"];
2480
2687
  let source = "";
2481
2688
  if (!noAppUrl && !appUrl) {
2482
2689
  const detected = await detectAppUrl(cwd2, appInfo.kind, env);
2483
2690
  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") {
2691
+ appUrl = detected.url || void 0;
2493
2692
  }
2494
2693
  let tool = options["tool"];
2694
+ let tools = tool ? [tool] : [];
2495
2695
  if (!tool) {
2496
2696
  if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT)
2497
2697
  tool = "claude-code";
@@ -2506,9 +2706,20 @@ async function initCommand(cwd2, options = {}) {
2506
2706
  else
2507
2707
  tool = isYes ? "other" : "claude-code";
2508
2708
  if (!isYes && !options["tool"]) {
2509
- tool = await select("AI tool", ["claude-code", "cursor", "windsurf", "opencode", "antigravity", "other"], tool);
2709
+ const ALL = "all of them";
2710
+ const catalogue = ["claude-code", "cursor", "windsurf", "opencode", "antigravity", "other"];
2711
+ const picked = await multiSelect(
2712
+ "Which AI tools work in this repo?",
2713
+ [ALL, ...catalogue],
2714
+ [tool]
2715
+ );
2716
+ tools = picked.includes(ALL) ? catalogue : picked;
2717
+ tool = tools[0] ?? tool;
2510
2718
  }
2511
2719
  }
2720
+ if (tools.length === 0)
2721
+ tools = [tool];
2722
+ closePrompts();
2512
2723
  if (!isJson)
2513
2724
  console.log(`Detecting your stack... -> ${appInfo.kind} (${appInfo.evidence.join(", ")})`);
2514
2725
  let injected = false;
@@ -2533,13 +2744,27 @@ async function initCommand(cwd2, options = {}) {
2533
2744
  }
2534
2745
  }
2535
2746
  if (!options["no-inject"]) {
2536
- if (appInfo.kind === "vite") {
2747
+ const explicitHtml = options["html"];
2748
+ if (explicitHtml && appInfo.kind !== "vite") {
2749
+ const htmlPath = await injectStatic(cwd2, explicitHtml, {
2750
+ server,
2751
+ key: finalProjectKey,
2752
+ environment: env,
2753
+ pin,
2754
+ envMap,
2755
+ environments: envs
2756
+ });
2757
+ filesMod = [htmlPath];
2758
+ injected = true;
2759
+ if (!isJson)
2760
+ console.log(`Injected widget into ${htmlPath}`);
2761
+ } else if (appInfo.kind === "vite") {
2537
2762
  filesMod = await injectVite(cwd2, { server, key: finalProjectKey, environment: env, pin }, options["html"]);
2538
2763
  injected = true;
2539
2764
  if (!isJson)
2540
2765
  console.log(`Injected widget into ${filesMod.join(", ")}`);
2541
2766
  } else if (appInfo.kind === "static") {
2542
- const htmlPath = await injectStatic(cwd2, options["html"], { server, key: finalProjectKey, environment: env, pin });
2767
+ const htmlPath = await injectStatic(cwd2, options["html"], { server, key: finalProjectKey, environment: env, pin, envMap, environments: envs });
2543
2768
  filesMod = [htmlPath];
2544
2769
  injected = true;
2545
2770
  if (!isJson)
@@ -2547,18 +2772,23 @@ async function initCommand(cwd2, options = {}) {
2547
2772
  } else if (appInfo.kind !== "unknown") {
2548
2773
  routedToSkill = true;
2549
2774
  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:
2775
+ console.log(`\u2139 ${appInfo.kind} detected \u2014 there's no single entry point to inject into automatically.
2776
+ If you know the file, name it and re-run \u2014 that always wins over detection:
2777
+ npx -y pointer-feedback init --html path/to/index.html
2778
+ Otherwise the pointer-init skill was installed for ${tool}; run it and it will mount the widget:
2552
2779
  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.`);
2780
+ Config is already saved in .pointer/config.json, so neither will ask for the key or project again.`);
2554
2781
  }
2555
2782
  }
2556
2783
  }
2557
2784
  if (!options["no-skills"]) {
2558
2785
  if (!isJson)
2559
- console.log("Installing AI skills");
2786
+ console.log(`Installing AI skills for ${tools.join(", ")}`);
2560
2787
  const skillsDir = options["skills-dir"];
2561
- const installed = await installSkills(server, tool, cwd2, skillsDir);
2788
+ const installed = [];
2789
+ for (const t of tools) {
2790
+ installed.push(...await installSkills(server, t, cwd2, t === tool ? skillsDir : void 0));
2791
+ }
2562
2792
  filesMod.push(...installed);
2563
2793
  skillFiles = installed.filter((f) => f.includes("SKILL.md") || f.endsWith(".md"));
2564
2794
  }
@@ -2584,7 +2814,8 @@ async function initCommand(cwd2, options = {}) {
2584
2814
  }
2585
2815
  const mergedStack = mergeStack(stackMeta, serverStackResponse?.data ?? serverStackResponse, noDesign ? null : designBlock);
2586
2816
  await writeStackFile(cwd2, mergedStack);
2587
- await writeConfig(cwd2, { server, project: finalProjectKey, environment: env, aiTool: tool, skillsDir: options["skills-dir"], cliVersion: BUILD_CLI_VERSION });
2817
+ const injectedHtml = injected ? filesMod.find((f) => f.toLowerCase().endsWith(".html"))?.replace(`${cwd2}/`, "") : void 0;
2818
+ await writeConfig(cwd2, { server, project: finalProjectKey, environment: env, aiTool: tool, skillsDir: options["skills-dir"], cliVersion: BUILD_CLI_VERSION, htmlPath: injectedHtml, environments: envs.length > 1 ? envs : void 0 });
2588
2819
  filesMod.push(".pointer/config.json");
2589
2820
  filesMod.push(".pointer/credentials.env");
2590
2821
  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.4",
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",