@kenkaiiii/gg-pixel 4.3.72 → 4.3.74

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 CHANGED
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/install.ts
4
- import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from "fs";
4
+ import {
5
+ existsSync,
6
+ readFileSync,
7
+ writeFileSync,
8
+ appendFileSync,
9
+ mkdirSync,
10
+ readdirSync
11
+ } from "fs";
5
12
  import { homedir } from "os";
6
13
  import { dirname, join, relative, resolve, sep } from "path";
7
14
  import { spawnSync } from "child_process";
@@ -13,17 +20,25 @@ async function install(opts = {}) {
13
20
  const home = opts.homeDir ?? homedir();
14
21
  const nodeRoot = findProjectRoot(cwd);
15
22
  const pythonRoot = findPythonProjectRoot(cwd);
16
- if (!nodeRoot && !pythonRoot) {
23
+ const goRoot = findGoProjectRoot(cwd);
24
+ const rubyRoot = findRubyProjectRoot(cwd);
25
+ if (!nodeRoot && !pythonRoot && !goRoot && !rubyRoot) {
17
26
  throw new Error(
18
- `No project found at ${cwd}: looked for package.json (Node/JS), pyproject.toml, setup.py, requirements.txt, Pipfile (Python).`
27
+ `No project found at ${cwd}: looked for package.json, pyproject.toml/setup.py/requirements.txt/Pipfile, go.mod, Gemfile/*.gemspec.`
19
28
  );
20
29
  }
21
- const useNode = pickCloserRoot(nodeRoot, pythonRoot) === nodeRoot;
22
- if (!useNode && pythonRoot) {
30
+ const closestRoot = pickClosestRoot([nodeRoot, pythonRoot, goRoot, rubyRoot]);
31
+ if (closestRoot === goRoot && goRoot) {
32
+ return installGo({ projectRoot: goRoot, opts, ingestUrl, fetchFn, home });
33
+ }
34
+ if (closestRoot === rubyRoot && rubyRoot) {
35
+ return installRuby({ projectRoot: rubyRoot, opts, ingestUrl, fetchFn, home });
36
+ }
37
+ if (closestRoot === pythonRoot && pythonRoot) {
23
38
  return installPython({ projectRoot: pythonRoot, opts, ingestUrl, fetchFn, home });
24
39
  }
25
40
  if (!nodeRoot) {
26
- throw new Error("Internal: no nodeRoot but useNode==true");
41
+ throw new Error("Internal: closest root is Node but nodeRoot is null");
27
42
  }
28
43
  const pkgPath = join(nodeRoot, "package.json");
29
44
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
@@ -259,6 +274,9 @@ function isCommonJsEntry(entryPath, pkg) {
259
274
  }
260
275
  function detectJsProjectKind(pkg, projectRoot) {
261
276
  const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
277
+ if (existsSync(join(projectRoot, "wrangler.toml")) || existsSync(join(projectRoot, "wrangler.jsonc")) || existsSync(join(projectRoot, "wrangler.json"))) {
278
+ return "cloudflare-workers";
279
+ }
262
280
  if ("electron" in all) return "electron";
263
281
  if (existsSync(join(projectRoot, "src-tauri")) || "@tauri-apps/api" in all) return "tauri";
264
282
  if ("react-native" in all) return "react-native";
@@ -289,8 +307,12 @@ function wireFramework(w) {
289
307
  return wireTauri(w);
290
308
  case "react-native":
291
309
  return wireReactNative(w);
310
+ case "cloudflare-workers":
311
+ return wireWorkers(w);
292
312
  case "python":
293
- throw new Error("Internal: python should have been handled earlier");
313
+ case "go":
314
+ case "ruby":
315
+ throw new Error(`Internal: ${w.kind} should have been handled earlier`);
294
316
  }
295
317
  }
296
318
  function wireNode({ projectRoot, pkg, ingestUrl }) {
@@ -316,6 +338,7 @@ function wireNextjs({ projectRoot, projectKey, ingestUrl }) {
316
338
  const serverInitPath = pickPath(projectRoot, ["instrumentation.ts", "instrumentation.js"]);
317
339
  const finalServerPath = serverInitPath ?? join(projectRoot, "instrumentation.ts");
318
340
  writeNextInstrumentation(finalServerPath, ingestUrl);
341
+ patchNextConfig(projectRoot);
319
342
  const clientInitPath = join(projectRoot, "gg-pixel.client.mjs");
320
343
  writeFileSync(clientInitPath, renderBrowserInitFile(ingestUrl, projectKey), "utf8");
321
344
  const layoutPath = findNextLayout(projectRoot);
@@ -388,6 +411,56 @@ function findNextLayout(projectRoot) {
388
411
  }
389
412
  return null;
390
413
  }
414
+ function patchNextConfig(projectRoot) {
415
+ const candidates = ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.cjs"];
416
+ let configPath = null;
417
+ for (const c of candidates) {
418
+ const p = join(projectRoot, c);
419
+ if (existsSync(p)) {
420
+ configPath = p;
421
+ break;
422
+ }
423
+ }
424
+ if (!configPath) {
425
+ configPath = join(projectRoot, "next.config.ts");
426
+ writeFileSync(
427
+ configPath,
428
+ `import type { NextConfig } from "next";
429
+
430
+ const nextConfig: NextConfig = {
431
+ // Keeps Next's bundler from trying to compile better-sqlite3 (native dep).
432
+ serverExternalPackages: ["@kenkaiiii/gg-pixel"],
433
+ };
434
+
435
+ export default nextConfig;
436
+ `,
437
+ "utf8"
438
+ );
439
+ return;
440
+ }
441
+ const content = readFileSync(configPath, "utf8");
442
+ if (content.includes("@kenkaiiii/gg-pixel")) return;
443
+ if (content.includes("serverExternalPackages")) {
444
+ const updated = content.replace(
445
+ /serverExternalPackages\s*:\s*\[([^\]]*)\]/,
446
+ (_match, inside) => {
447
+ const trimmed = inside.trim();
448
+ const sep2 = trimmed.length > 0 ? ", " : "";
449
+ return `serverExternalPackages: [${trimmed}${sep2}"@kenkaiiii/gg-pixel"]`;
450
+ }
451
+ );
452
+ if (updated !== content) writeFileSync(configPath, updated, "utf8");
453
+ return;
454
+ }
455
+ const objStart = /(const\s+\w+\s*:\s*NextConfig\s*=\s*\{|module\.exports\s*=\s*\{|export\s+default\s*\{)/;
456
+ const m = objStart.exec(content);
457
+ if (m) {
458
+ const insertAt = m.index + m[0].length;
459
+ const updated = content.slice(0, insertAt) + `
460
+ serverExternalPackages: ["@kenkaiiii/gg-pixel"],` + content.slice(insertAt);
461
+ writeFileSync(configPath, updated, "utf8");
462
+ }
463
+ }
391
464
  function wireSveltekit({ projectRoot, projectKey, ingestUrl }) {
392
465
  const serverPath = join(projectRoot, "src/hooks.server.ts");
393
466
  const clientPath = join(projectRoot, "src/hooks.client.ts");
@@ -549,6 +622,48 @@ function wireTauri({ projectRoot, pkg, projectKey, ingestUrl }) {
549
622
  ]
550
623
  };
551
624
  }
625
+ function wireWorkers({ projectRoot, projectKey, ingestUrl }) {
626
+ const initPath = join(projectRoot, "gg-pixel.workers.snippet.ts");
627
+ writeFileSync(
628
+ initPath,
629
+ `// gg-pixel \u2014 Cloudflare Workers wiring snippet.
630
+ // Auto-generated by ggcoder pixel install. Wrap your default export with
631
+ // withPixel(...) so any throw in your handler is auto-reported. Example:
632
+ //
633
+ // import { withPixel } from "@kenkaiiii/gg-pixel/workers";
634
+ //
635
+ // export default withPixel(
636
+ // { projectKey: ${JSON.stringify(projectKey)} },
637
+ // {
638
+ // async fetch(req, env, ctx) { /* your code */ },
639
+ // async scheduled(evt, env, ctx) { /* your code */ },
640
+ // },
641
+ // );
642
+ //
643
+ // For manual reports inside a handler:
644
+ //
645
+ // import { reportPixel } from "@kenkaiiii/gg-pixel/workers";
646
+ // reportPixel(ctx, { projectKey: ${JSON.stringify(projectKey)} }, {
647
+ // message: "user clicked the broken button",
648
+ // });
649
+ //
650
+ // Your project_key is publishable \u2014 safe to commit.
651
+
652
+ import { withPixel, reportPixel } from "@kenkaiiii/gg-pixel/workers";
653
+ export const PIXEL_KEY = ${JSON.stringify(projectKey)};
654
+ export const PIXEL_INGEST = ${JSON.stringify(ingestUrl)};
655
+ export { withPixel, reportPixel };
656
+ `,
657
+ "utf8"
658
+ );
659
+ return {
660
+ primaryInitPath: initPath,
661
+ entryWiring: { kind: "no_entry_found" },
662
+ warnings: [
663
+ `Cloudflare Workers default exports can't be auto-wrapped safely. Open ${initPath} for a 3-line snippet you can paste into your worker.`
664
+ ]
665
+ };
666
+ }
552
667
  function wireReactNative({ projectRoot }) {
553
668
  return {
554
669
  primaryInitPath: join(projectRoot, "(not-installed)"),
@@ -654,10 +769,191 @@ function detectPythonPackageManager(projectRoot) {
654
769
  if (existsSync(join(projectRoot, "Pipfile.lock"))) return "pipenv";
655
770
  return "pip";
656
771
  }
657
- function pickCloserRoot(a, b) {
658
- if (!a) return b;
659
- if (!b) return a;
660
- return a.length >= b.length ? a : b;
772
+ function pickClosestRoot(roots) {
773
+ let best = null;
774
+ for (const r of roots) {
775
+ if (!r) continue;
776
+ if (!best || r.length > best.length) best = r;
777
+ }
778
+ return best;
779
+ }
780
+ var GO_MARKER = "go.mod";
781
+ var RUBY_MARKERS = ["Gemfile"];
782
+ function findGoProjectRoot(start) {
783
+ let dir = start;
784
+ for (let i = 0; i < 20; i++) {
785
+ if (existsSync(join(dir, GO_MARKER))) return dir;
786
+ const parent = dirname(dir);
787
+ if (parent === dir) return null;
788
+ dir = parent;
789
+ }
790
+ return null;
791
+ }
792
+ function findRubyProjectRoot(start) {
793
+ let dir = start;
794
+ for (let i = 0; i < 20; i++) {
795
+ for (const m of RUBY_MARKERS) {
796
+ if (existsSync(join(dir, m))) return dir;
797
+ }
798
+ try {
799
+ const entries = readdirSync(dir);
800
+ if (entries.some((e) => e.endsWith(".gemspec"))) return dir;
801
+ } catch {
802
+ }
803
+ const parent = dirname(dir);
804
+ if (parent === dir) return null;
805
+ dir = parent;
806
+ }
807
+ return null;
808
+ }
809
+ async function installGo(ctx) {
810
+ const { projectRoot, opts, ingestUrl, fetchFn, home } = ctx;
811
+ const projectName = opts.projectName ?? readGoModuleName(projectRoot) ?? projectRoot.split("/").pop() ?? "unnamed";
812
+ const projectsJsonPath = join(home, ".gg", "projects.json");
813
+ const envFilePath = join(projectRoot, ".env");
814
+ const existing = findMappingByPath(projectsJsonPath, projectRoot);
815
+ const existingKey = readEnvKey(envFilePath, "GG_PIXEL_KEY");
816
+ let created;
817
+ let reused = false;
818
+ if (existing && existingKey) {
819
+ created = { id: existing.id, key: existingKey };
820
+ reused = true;
821
+ } else {
822
+ created = await createProject(fetchFn, ingestUrl, projectName);
823
+ }
824
+ const packageInstalled = opts.skipPackageInstall ? false : runGoGet(projectRoot);
825
+ const initFilePath = join(projectRoot, "gg_pixel_init.go");
826
+ writeFileSync(
827
+ initFilePath,
828
+ `// gg-pixel init \u2014 auto-generated by ggcoder pixel install.
829
+ package main
830
+
831
+ import (
832
+ "os"
833
+ gg "github.com/kenkaiiii/gg-pixel-go"
834
+ )
835
+
836
+ func init() {
837
+ key := os.Getenv("GG_PIXEL_KEY")
838
+ if key == "" {
839
+ key = ${JSON.stringify(created.key)}
840
+ }
841
+ _ = gg.Init(gg.Options{ProjectKey: key, IngestURL: ${JSON.stringify(`${ingestUrl}/ingest`)}})
842
+ }
843
+ `,
844
+ "utf8"
845
+ );
846
+ writeEnvKey(envFilePath, "GG_PIXEL_KEY", created.key);
847
+ writeProjectsMapping(projectsJsonPath, created.id, projectName, projectRoot);
848
+ return {
849
+ projectId: created.id,
850
+ projectKey: created.key,
851
+ projectName,
852
+ projectKind: "go",
853
+ initFilePath,
854
+ envFilePath,
855
+ projectsJsonPath,
856
+ packageManager: "pip",
857
+ packageInstalled,
858
+ entryWiring: { kind: "no_entry_found" },
859
+ reused,
860
+ warnings: [
861
+ "Add `defer ggpixel.Recover()` near the top of your main() so panics are captured before the process exits."
862
+ ]
863
+ };
864
+ }
865
+ function readGoModuleName(projectRoot) {
866
+ try {
867
+ const content = readFileSync(join(projectRoot, "go.mod"), "utf8");
868
+ const match = /^\s*module\s+(\S+)\s*$/m.exec(content);
869
+ if (!match) return null;
870
+ return match[1].split("/").pop() ?? null;
871
+ } catch {
872
+ return null;
873
+ }
874
+ }
875
+ function runGoGet(projectRoot) {
876
+ const result = spawnSync("go", ["get", "github.com/kenkaiiii/gg-pixel-go@latest"], {
877
+ cwd: projectRoot,
878
+ stdio: "inherit"
879
+ });
880
+ return result.status === 0;
881
+ }
882
+ async function installRuby(ctx) {
883
+ const { projectRoot, opts, ingestUrl, fetchFn, home } = ctx;
884
+ const projectName = opts.projectName ?? readRubyAppName(projectRoot) ?? projectRoot.split("/").pop() ?? "unnamed";
885
+ const projectsJsonPath = join(home, ".gg", "projects.json");
886
+ const envFilePath = join(projectRoot, ".env");
887
+ const existing = findMappingByPath(projectsJsonPath, projectRoot);
888
+ const existingKey = readEnvKey(envFilePath, "GG_PIXEL_KEY");
889
+ let created;
890
+ let reused = false;
891
+ if (existing && existingKey) {
892
+ created = { id: existing.id, key: existingKey };
893
+ reused = true;
894
+ } else {
895
+ created = await createProject(fetchFn, ingestUrl, projectName);
896
+ }
897
+ const packageInstalled = opts.skipPackageInstall ? false : runRubyInstall(projectRoot);
898
+ const initFilePath = join(projectRoot, "gg_pixel_init.rb");
899
+ writeFileSync(
900
+ initFilePath,
901
+ `# gg-pixel init \u2014 auto-generated by ggcoder pixel install.
902
+ require "gg_pixel"
903
+ GGPixel.init(
904
+ project_key: ENV["GG_PIXEL_KEY"] || ${JSON.stringify(created.key)},
905
+ ingest_url: ${JSON.stringify(`${ingestUrl}/ingest`)},
906
+ )
907
+ `,
908
+ "utf8"
909
+ );
910
+ writeEnvKey(envFilePath, "GG_PIXEL_KEY", created.key);
911
+ writeProjectsMapping(projectsJsonPath, created.id, projectName, projectRoot);
912
+ return {
913
+ projectId: created.id,
914
+ projectKey: created.key,
915
+ projectName,
916
+ projectKind: "ruby",
917
+ initFilePath,
918
+ envFilePath,
919
+ projectsJsonPath,
920
+ packageManager: "pip",
921
+ packageInstalled,
922
+ entryWiring: { kind: "no_entry_found" },
923
+ reused,
924
+ warnings: [
925
+ `Add \`require "./gg_pixel_init"\` at the top of your entry script (often \`config/application.rb\` for Rails, \`app.rb\` for Sinatra, or your main file).`
926
+ ]
927
+ };
928
+ }
929
+ function readRubyAppName(projectRoot) {
930
+ try {
931
+ const entries = readdirSync(projectRoot);
932
+ const gemspec = entries.find((e) => e.endsWith(".gemspec"));
933
+ if (!gemspec) return null;
934
+ return gemspec.replace(/\.gemspec$/, "");
935
+ } catch {
936
+ return null;
937
+ }
938
+ }
939
+ function runRubyInstall(projectRoot) {
940
+ if (existsSync(join(projectRoot, "Gemfile"))) {
941
+ try {
942
+ const content = readFileSync(join(projectRoot, "Gemfile"), "utf8");
943
+ if (!content.includes("gg_pixel")) {
944
+ writeFileSync(
945
+ join(projectRoot, "Gemfile"),
946
+ content + (content.endsWith("\n") ? "" : "\n") + 'gem "gg_pixel"\n',
947
+ "utf8"
948
+ );
949
+ }
950
+ } catch {
951
+ }
952
+ const r = spawnSync("bundle", ["install"], { cwd: projectRoot, stdio: "inherit" });
953
+ if (r.status === 0) return true;
954
+ }
955
+ const r2 = spawnSync("gem", ["install", "gg_pixel"], { cwd: projectRoot, stdio: "inherit" });
956
+ return r2.status === 0;
661
957
  }
662
958
  async function installPython(ctx) {
663
959
  const { projectRoot, opts, ingestUrl, fetchFn, home } = ctx;