@kenkaiiii/gg-pixel 4.3.72 → 4.3.73

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 }) {
@@ -549,6 +571,48 @@ function wireTauri({ projectRoot, pkg, projectKey, ingestUrl }) {
549
571
  ]
550
572
  };
551
573
  }
574
+ function wireWorkers({ projectRoot, projectKey, ingestUrl }) {
575
+ const initPath = join(projectRoot, "gg-pixel.workers.snippet.ts");
576
+ writeFileSync(
577
+ initPath,
578
+ `// gg-pixel \u2014 Cloudflare Workers wiring snippet.
579
+ // Auto-generated by ggcoder pixel install. Wrap your default export with
580
+ // withPixel(...) so any throw in your handler is auto-reported. Example:
581
+ //
582
+ // import { withPixel } from "@kenkaiiii/gg-pixel/workers";
583
+ //
584
+ // export default withPixel(
585
+ // { projectKey: ${JSON.stringify(projectKey)} },
586
+ // {
587
+ // async fetch(req, env, ctx) { /* your code */ },
588
+ // async scheduled(evt, env, ctx) { /* your code */ },
589
+ // },
590
+ // );
591
+ //
592
+ // For manual reports inside a handler:
593
+ //
594
+ // import { reportPixel } from "@kenkaiiii/gg-pixel/workers";
595
+ // reportPixel(ctx, { projectKey: ${JSON.stringify(projectKey)} }, {
596
+ // message: "user clicked the broken button",
597
+ // });
598
+ //
599
+ // Your project_key is publishable \u2014 safe to commit.
600
+
601
+ import { withPixel, reportPixel } from "@kenkaiiii/gg-pixel/workers";
602
+ export const PIXEL_KEY = ${JSON.stringify(projectKey)};
603
+ export const PIXEL_INGEST = ${JSON.stringify(ingestUrl)};
604
+ export { withPixel, reportPixel };
605
+ `,
606
+ "utf8"
607
+ );
608
+ return {
609
+ primaryInitPath: initPath,
610
+ entryWiring: { kind: "no_entry_found" },
611
+ warnings: [
612
+ `Cloudflare Workers default exports can't be auto-wrapped safely. Open ${initPath} for a 3-line snippet you can paste into your worker.`
613
+ ]
614
+ };
615
+ }
552
616
  function wireReactNative({ projectRoot }) {
553
617
  return {
554
618
  primaryInitPath: join(projectRoot, "(not-installed)"),
@@ -654,10 +718,191 @@ function detectPythonPackageManager(projectRoot) {
654
718
  if (existsSync(join(projectRoot, "Pipfile.lock"))) return "pipenv";
655
719
  return "pip";
656
720
  }
657
- function pickCloserRoot(a, b) {
658
- if (!a) return b;
659
- if (!b) return a;
660
- return a.length >= b.length ? a : b;
721
+ function pickClosestRoot(roots) {
722
+ let best = null;
723
+ for (const r of roots) {
724
+ if (!r) continue;
725
+ if (!best || r.length > best.length) best = r;
726
+ }
727
+ return best;
728
+ }
729
+ var GO_MARKER = "go.mod";
730
+ var RUBY_MARKERS = ["Gemfile"];
731
+ function findGoProjectRoot(start) {
732
+ let dir = start;
733
+ for (let i = 0; i < 20; i++) {
734
+ if (existsSync(join(dir, GO_MARKER))) return dir;
735
+ const parent = dirname(dir);
736
+ if (parent === dir) return null;
737
+ dir = parent;
738
+ }
739
+ return null;
740
+ }
741
+ function findRubyProjectRoot(start) {
742
+ let dir = start;
743
+ for (let i = 0; i < 20; i++) {
744
+ for (const m of RUBY_MARKERS) {
745
+ if (existsSync(join(dir, m))) return dir;
746
+ }
747
+ try {
748
+ const entries = readdirSync(dir);
749
+ if (entries.some((e) => e.endsWith(".gemspec"))) return dir;
750
+ } catch {
751
+ }
752
+ const parent = dirname(dir);
753
+ if (parent === dir) return null;
754
+ dir = parent;
755
+ }
756
+ return null;
757
+ }
758
+ async function installGo(ctx) {
759
+ const { projectRoot, opts, ingestUrl, fetchFn, home } = ctx;
760
+ const projectName = opts.projectName ?? readGoModuleName(projectRoot) ?? projectRoot.split("/").pop() ?? "unnamed";
761
+ const projectsJsonPath = join(home, ".gg", "projects.json");
762
+ const envFilePath = join(projectRoot, ".env");
763
+ const existing = findMappingByPath(projectsJsonPath, projectRoot);
764
+ const existingKey = readEnvKey(envFilePath, "GG_PIXEL_KEY");
765
+ let created;
766
+ let reused = false;
767
+ if (existing && existingKey) {
768
+ created = { id: existing.id, key: existingKey };
769
+ reused = true;
770
+ } else {
771
+ created = await createProject(fetchFn, ingestUrl, projectName);
772
+ }
773
+ const packageInstalled = opts.skipPackageInstall ? false : runGoGet(projectRoot);
774
+ const initFilePath = join(projectRoot, "gg_pixel_init.go");
775
+ writeFileSync(
776
+ initFilePath,
777
+ `// gg-pixel init \u2014 auto-generated by ggcoder pixel install.
778
+ package main
779
+
780
+ import (
781
+ "os"
782
+ gg "github.com/kenkaiiii/gg-pixel-go"
783
+ )
784
+
785
+ func init() {
786
+ key := os.Getenv("GG_PIXEL_KEY")
787
+ if key == "" {
788
+ key = ${JSON.stringify(created.key)}
789
+ }
790
+ _ = gg.Init(gg.Options{ProjectKey: key, IngestURL: ${JSON.stringify(`${ingestUrl}/ingest`)}})
791
+ }
792
+ `,
793
+ "utf8"
794
+ );
795
+ writeEnvKey(envFilePath, "GG_PIXEL_KEY", created.key);
796
+ writeProjectsMapping(projectsJsonPath, created.id, projectName, projectRoot);
797
+ return {
798
+ projectId: created.id,
799
+ projectKey: created.key,
800
+ projectName,
801
+ projectKind: "go",
802
+ initFilePath,
803
+ envFilePath,
804
+ projectsJsonPath,
805
+ packageManager: "pip",
806
+ packageInstalled,
807
+ entryWiring: { kind: "no_entry_found" },
808
+ reused,
809
+ warnings: [
810
+ "Add `defer ggpixel.Recover()` near the top of your main() so panics are captured before the process exits."
811
+ ]
812
+ };
813
+ }
814
+ function readGoModuleName(projectRoot) {
815
+ try {
816
+ const content = readFileSync(join(projectRoot, "go.mod"), "utf8");
817
+ const match = /^\s*module\s+(\S+)\s*$/m.exec(content);
818
+ if (!match) return null;
819
+ return match[1].split("/").pop() ?? null;
820
+ } catch {
821
+ return null;
822
+ }
823
+ }
824
+ function runGoGet(projectRoot) {
825
+ const result = spawnSync("go", ["get", "github.com/kenkaiiii/gg-pixel-go@latest"], {
826
+ cwd: projectRoot,
827
+ stdio: "inherit"
828
+ });
829
+ return result.status === 0;
830
+ }
831
+ async function installRuby(ctx) {
832
+ const { projectRoot, opts, ingestUrl, fetchFn, home } = ctx;
833
+ const projectName = opts.projectName ?? readRubyAppName(projectRoot) ?? projectRoot.split("/").pop() ?? "unnamed";
834
+ const projectsJsonPath = join(home, ".gg", "projects.json");
835
+ const envFilePath = join(projectRoot, ".env");
836
+ const existing = findMappingByPath(projectsJsonPath, projectRoot);
837
+ const existingKey = readEnvKey(envFilePath, "GG_PIXEL_KEY");
838
+ let created;
839
+ let reused = false;
840
+ if (existing && existingKey) {
841
+ created = { id: existing.id, key: existingKey };
842
+ reused = true;
843
+ } else {
844
+ created = await createProject(fetchFn, ingestUrl, projectName);
845
+ }
846
+ const packageInstalled = opts.skipPackageInstall ? false : runRubyInstall(projectRoot);
847
+ const initFilePath = join(projectRoot, "gg_pixel_init.rb");
848
+ writeFileSync(
849
+ initFilePath,
850
+ `# gg-pixel init \u2014 auto-generated by ggcoder pixel install.
851
+ require "gg_pixel"
852
+ GGPixel.init(
853
+ project_key: ENV["GG_PIXEL_KEY"] || ${JSON.stringify(created.key)},
854
+ ingest_url: ${JSON.stringify(`${ingestUrl}/ingest`)},
855
+ )
856
+ `,
857
+ "utf8"
858
+ );
859
+ writeEnvKey(envFilePath, "GG_PIXEL_KEY", created.key);
860
+ writeProjectsMapping(projectsJsonPath, created.id, projectName, projectRoot);
861
+ return {
862
+ projectId: created.id,
863
+ projectKey: created.key,
864
+ projectName,
865
+ projectKind: "ruby",
866
+ initFilePath,
867
+ envFilePath,
868
+ projectsJsonPath,
869
+ packageManager: "pip",
870
+ packageInstalled,
871
+ entryWiring: { kind: "no_entry_found" },
872
+ reused,
873
+ warnings: [
874
+ `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).`
875
+ ]
876
+ };
877
+ }
878
+ function readRubyAppName(projectRoot) {
879
+ try {
880
+ const entries = readdirSync(projectRoot);
881
+ const gemspec = entries.find((e) => e.endsWith(".gemspec"));
882
+ if (!gemspec) return null;
883
+ return gemspec.replace(/\.gemspec$/, "");
884
+ } catch {
885
+ return null;
886
+ }
887
+ }
888
+ function runRubyInstall(projectRoot) {
889
+ if (existsSync(join(projectRoot, "Gemfile"))) {
890
+ try {
891
+ const content = readFileSync(join(projectRoot, "Gemfile"), "utf8");
892
+ if (!content.includes("gg_pixel")) {
893
+ writeFileSync(
894
+ join(projectRoot, "Gemfile"),
895
+ content + (content.endsWith("\n") ? "" : "\n") + 'gem "gg_pixel"\n',
896
+ "utf8"
897
+ );
898
+ }
899
+ } catch {
900
+ }
901
+ const r = spawnSync("bundle", ["install"], { cwd: projectRoot, stdio: "inherit" });
902
+ if (r.status === 0) return true;
903
+ }
904
+ const r2 = spawnSync("gem", ["install", "gg_pixel"], { cwd: projectRoot, stdio: "inherit" });
905
+ return r2.status === 0;
661
906
  }
662
907
  async function installPython(ctx) {
663
908
  const { projectRoot, opts, ingestUrl, fetchFn, home } = ctx;