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.
- package/dist/cli.js +306 -75
- 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.
|
|
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
|
-
|
|
872
|
-
const
|
|
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
|
-
|
|
911
|
+
const pending = rl.question(displayQuestion);
|
|
887
912
|
if (options.secret)
|
|
888
913
|
muted = true;
|
|
889
|
-
|
|
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
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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 -->` :
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2551
|
-
|
|
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
|
|
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(
|
|
2786
|
+
console.log(`Installing AI skills for ${tools.join(", ")}`);
|
|
2560
2787
|
const skillsDir = options["skills-dir"];
|
|
2561
|
-
const installed =
|
|
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
|
-
|
|
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