@glasstrace/sdk 0.19.0 → 0.20.0

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.
@@ -53,7 +53,7 @@ declare function decideMcpConfigAction(options: {
53
53
  * Identifies a scaffolding step that can be reversed during rollback.
54
54
  * Steps are tracked in execution order and rolled back in reverse.
55
55
  */
56
- type CompletedStep = "instrumentation" | "next-config" | "env-local" | "gitignore";
56
+ type CompletedStep = "instrumentation" | "next-config" | "env-local" | "gitignore" | "discovery-file";
57
57
  /**
58
58
  * Tracks state needed for accurate rollback of init steps.
59
59
  * Separating this from the step list allows rollback to restore
@@ -74,6 +74,52 @@ interface RollbackState {
74
74
  * When present, rollback restores this instead of using removeRegisterGlasstrace. */
75
75
  originalInstrumentationContent?: string;
76
76
  }
77
+ /**
78
+ * Returns true when the given `.gitignore` content would exclude the
79
+ * static discovery file at `<root>/public/.well-known/glasstrace.json`
80
+ * or `<root>/static/.well-known/glasstrace.json` (depending on `layout`)
81
+ * from being committed.
82
+ *
83
+ * Model: the file has three ancestors in its committed path — the
84
+ * static root directory (`public/` or `static/`), the `.well-known/`
85
+ * sub-directory, and the file itself. Each pattern in `.gitignore` is
86
+ * classified by which ancestor paths it matches, and each ancestor
87
+ * carries its own current ignore state that later patterns can flip.
88
+ * Patterns that don't match any of the three ancestors are ignored.
89
+ *
90
+ * Per `gitignore(5)`:
91
+ *
92
+ * > It is not possible to re-include a file if a parent directory of
93
+ * > that file is excluded.
94
+ *
95
+ * Consequently, the file is reported as ignored when the final state
96
+ * of ANY ancestor (static root, `.well-known/`, or the file itself)
97
+ * is ignored — a `!<file>` negation alone cannot "escape" an ignored
98
+ * ancestor.
99
+ *
100
+ * Per-ancestor tracking is what distinguishes overlapping patterns:
101
+ *
102
+ * - `public/` then `!public/` — root re-included, file OK.
103
+ * - `public/` then `!public/.well-known/` — root still ignored (scope-2
104
+ * negation doesn't match the scope-1 ancestor path), file ignored.
105
+ * - `.well-known/` then `!public/.well-known/` — `.well-known/` re-included
106
+ * (both patterns match the scope-2 ancestor), file OK.
107
+ * - `.well-known/` then `!public/` — `!public/` matches the
108
+ * scope-1 ancestor only; the scope-2 ancestor (`.well-known/`) is still
109
+ * ignored, so the file is ignored.
110
+ * - `<file>` then `!<file>` — file-level ignore flipped.
111
+ * - `<file>` then `!public/.well-known/` — directory negation does
112
+ * not match the file path; file still ignored.
113
+ *
114
+ * Not-modeled rules — glob wildcards (star, question mark, character
115
+ * classes), overlapping any-depth matches across multiple parents, and
116
+ * nested `.gitignore` files — skew the heuristic toward false positives
117
+ * (extra warning output) rather than false negatives. The warning is
118
+ * advisory.
119
+ *
120
+ * @internal Exported for unit testing only.
121
+ */
122
+ declare function gitignoreExcludesDiscoveryFile(gitignoreContent: string, layout: "public" | "static"): boolean;
77
123
  /**
78
124
  * Best-effort rollback of completed init steps in reverse order.
79
125
  * Each step is individually try/caught so that a failure in one
@@ -132,4 +178,4 @@ type VerifyAnonKeyOutcome = {
132
178
  */
133
179
  declare function verifyAnonKeyRegistration(projectRoot: string): Promise<VerifyAnonKeyOutcome>;
134
180
 
135
- export { type InitOptions, type InitResult, type VerifyAnonKeyOutcome, decideMcpConfigAction, meetsNodeVersion, rollbackSteps, runInit, verifyAnonKeyRegistration };
181
+ export { type InitOptions, type InitResult, type VerifyAnonKeyOutcome, decideMcpConfigAction, gitignoreExcludesDiscoveryFile, meetsNodeVersion, rollbackSteps, runInit, verifyAnonKeyRegistration };
@@ -53,7 +53,7 @@ declare function decideMcpConfigAction(options: {
53
53
  * Identifies a scaffolding step that can be reversed during rollback.
54
54
  * Steps are tracked in execution order and rolled back in reverse.
55
55
  */
56
- type CompletedStep = "instrumentation" | "next-config" | "env-local" | "gitignore";
56
+ type CompletedStep = "instrumentation" | "next-config" | "env-local" | "gitignore" | "discovery-file";
57
57
  /**
58
58
  * Tracks state needed for accurate rollback of init steps.
59
59
  * Separating this from the step list allows rollback to restore
@@ -74,6 +74,52 @@ interface RollbackState {
74
74
  * When present, rollback restores this instead of using removeRegisterGlasstrace. */
75
75
  originalInstrumentationContent?: string;
76
76
  }
77
+ /**
78
+ * Returns true when the given `.gitignore` content would exclude the
79
+ * static discovery file at `<root>/public/.well-known/glasstrace.json`
80
+ * or `<root>/static/.well-known/glasstrace.json` (depending on `layout`)
81
+ * from being committed.
82
+ *
83
+ * Model: the file has three ancestors in its committed path — the
84
+ * static root directory (`public/` or `static/`), the `.well-known/`
85
+ * sub-directory, and the file itself. Each pattern in `.gitignore` is
86
+ * classified by which ancestor paths it matches, and each ancestor
87
+ * carries its own current ignore state that later patterns can flip.
88
+ * Patterns that don't match any of the three ancestors are ignored.
89
+ *
90
+ * Per `gitignore(5)`:
91
+ *
92
+ * > It is not possible to re-include a file if a parent directory of
93
+ * > that file is excluded.
94
+ *
95
+ * Consequently, the file is reported as ignored when the final state
96
+ * of ANY ancestor (static root, `.well-known/`, or the file itself)
97
+ * is ignored — a `!<file>` negation alone cannot "escape" an ignored
98
+ * ancestor.
99
+ *
100
+ * Per-ancestor tracking is what distinguishes overlapping patterns:
101
+ *
102
+ * - `public/` then `!public/` — root re-included, file OK.
103
+ * - `public/` then `!public/.well-known/` — root still ignored (scope-2
104
+ * negation doesn't match the scope-1 ancestor path), file ignored.
105
+ * - `.well-known/` then `!public/.well-known/` — `.well-known/` re-included
106
+ * (both patterns match the scope-2 ancestor), file OK.
107
+ * - `.well-known/` then `!public/` — `!public/` matches the
108
+ * scope-1 ancestor only; the scope-2 ancestor (`.well-known/`) is still
109
+ * ignored, so the file is ignored.
110
+ * - `<file>` then `!<file>` — file-level ignore flipped.
111
+ * - `<file>` then `!public/.well-known/` — directory negation does
112
+ * not match the file path; file still ignored.
113
+ *
114
+ * Not-modeled rules — glob wildcards (star, question mark, character
115
+ * classes), overlapping any-depth matches across multiple parents, and
116
+ * nested `.gitignore` files — skew the heuristic toward false positives
117
+ * (extra warning output) rather than false negatives. The warning is
118
+ * advisory.
119
+ *
120
+ * @internal Exported for unit testing only.
121
+ */
122
+ declare function gitignoreExcludesDiscoveryFile(gitignoreContent: string, layout: "public" | "static"): boolean;
77
123
  /**
78
124
  * Best-effort rollback of completed init steps in reverse order.
79
125
  * Each step is individually try/caught so that a failure in one
@@ -132,4 +178,4 @@ type VerifyAnonKeyOutcome = {
132
178
  */
133
179
  declare function verifyAnonKeyRegistration(projectRoot: string): Promise<VerifyAnonKeyOutcome>;
134
180
 
135
- export { type InitOptions, type InitResult, type VerifyAnonKeyOutcome, decideMcpConfigAction, meetsNodeVersion, rollbackSteps, runInit, verifyAnonKeyRegistration };
181
+ export { type InitOptions, type InitResult, type VerifyAnonKeyOutcome, decideMcpConfigAction, gitignoreExcludesDiscoveryFile, meetsNodeVersion, rollbackSteps, runInit, verifyAnonKeyRegistration };
package/dist/cli/init.js CHANGED
@@ -11,11 +11,15 @@ import {
11
11
  } from "../chunk-VUZCLMIX.js";
12
12
  import {
13
13
  isInitCreatedInstrumentation,
14
+ relativeDiscoveryPath,
15
+ removeDiscoveryFile,
14
16
  removeGlasstraceConfigImport,
15
17
  removeRegisterGlasstrace,
18
+ resolveStaticRoot,
16
19
  unwrapCJSExport,
17
- unwrapExport
18
- } from "../chunk-XNDHQN4S.js";
20
+ unwrapExport,
21
+ writeDiscoveryFile
22
+ } from "../chunk-6JRI4OGB.js";
19
23
  import {
20
24
  detectAgents,
21
25
  generateInfoSection,
@@ -28,7 +32,6 @@ import {
28
32
  getOrCreateAnonKey,
29
33
  readAnonKey
30
34
  } from "../chunk-YPXW2TN3.js";
31
- import "../chunk-5N2IR4EO.js";
32
35
  import {
33
36
  addCoverageMapEnv,
34
37
  isDevApiKey,
@@ -41,6 +44,7 @@ import {
41
44
  scaffoldMcpMarker,
42
45
  scaffoldNextConfig
43
46
  } from "../chunk-O63DJKIJ.js";
47
+ import "../chunk-5N2IR4EO.js";
44
48
  import {
45
49
  MCP_ENDPOINT,
46
50
  NEXT_CONFIG_NAMES,
@@ -105,6 +109,53 @@ async function promptYesNo(question, defaultValue) {
105
109
  function cleanLeadingBlankLines(content) {
106
110
  return content.replace(/^\n{2,}/, "\n");
107
111
  }
112
+ function gitignoreExcludesDiscoveryFile(gitignoreContent, layout) {
113
+ const staticRoot = layout === "static" ? "static" : "public";
114
+ const rootTargets = [staticRoot, `${staticRoot}/`];
115
+ const wellKnownTargets = [
116
+ `${staticRoot}/.well-known`,
117
+ `${staticRoot}/.well-known/`,
118
+ ".well-known",
119
+ ".well-known/"
120
+ ];
121
+ const fileTargets = [
122
+ `${staticRoot}/.well-known/glasstrace.json`,
123
+ ".well-known/glasstrace.json"
124
+ ];
125
+ let rootIgnored = false;
126
+ let wellKnownIgnored = false;
127
+ let fileIgnored = false;
128
+ for (const rawLine of gitignoreContent.split("\n")) {
129
+ const line = rawLine.trim();
130
+ if (line === "" || line.startsWith("#")) continue;
131
+ const negation = line.startsWith("!");
132
+ const pattern = negation ? line.slice(1).trim() : line;
133
+ if (pattern === "") continue;
134
+ const normalized = pattern.startsWith("/") ? pattern.slice(1) : pattern;
135
+ const matchesRoot = matchesDiscoveryPath(normalized, rootTargets);
136
+ const matchesWellKnown = matchesDiscoveryPath(normalized, wellKnownTargets);
137
+ const matchesFile = matchesDiscoveryPath(normalized, fileTargets);
138
+ if (!matchesRoot && !matchesWellKnown && !matchesFile) continue;
139
+ const newState = !negation;
140
+ if (matchesRoot) rootIgnored = newState;
141
+ if (matchesWellKnown) wellKnownIgnored = newState;
142
+ if (matchesFile) fileIgnored = newState;
143
+ }
144
+ return rootIgnored || wellKnownIgnored || fileIgnored;
145
+ }
146
+ function matchesDiscoveryPath(pattern, targets) {
147
+ const bare = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
148
+ for (const target of targets) {
149
+ const tBare = target.endsWith("/") ? target.slice(0, -1) : target;
150
+ if (bare === tBare) return true;
151
+ if (pattern.startsWith("**/")) {
152
+ const suffix = pattern.slice(3);
153
+ const sBare = suffix.endsWith("/") ? suffix.slice(0, -1) : suffix;
154
+ if (tBare === sBare || tBare.endsWith(`/${sBare}`)) return true;
155
+ }
156
+ }
157
+ return false;
158
+ }
108
159
  async function rollbackSteps(steps, projectRoot, state) {
109
160
  for (const step of [...steps].reverse()) {
110
161
  try {
@@ -185,6 +236,10 @@ async function rollbackSteps(steps, projectRoot, state) {
185
236
  }
186
237
  break;
187
238
  }
239
+ case "discovery-file": {
240
+ removeDiscoveryFile(projectRoot);
241
+ break;
242
+ }
188
243
  }
189
244
  } catch {
190
245
  }
@@ -348,6 +403,50 @@ Then add this as the first statement in your register() function:
348
403
  if (preExistingAnonKey !== null) {
349
404
  summary.push("Preserved existing .glasstrace/anon_key");
350
405
  }
406
+ try {
407
+ const discoveryResult = writeDiscoveryFile(projectRoot, anonKey);
408
+ const relPath = relativeDiscoveryPath(discoveryResult.layout);
409
+ switch (discoveryResult.action) {
410
+ case "created":
411
+ summary.push(`Created ${relPath}`);
412
+ rollbackState.steps.push("discovery-file");
413
+ break;
414
+ case "updated-stale":
415
+ summary.push(`Updated ${relPath} (anon key had changed)`);
416
+ break;
417
+ case "skipped-matches":
418
+ summary.push(`Skipped ${relPath} (already matches anon key)`);
419
+ break;
420
+ case "skipped-foreign":
421
+ summary.push(
422
+ `Rewrote ${relPath} (existing file was malformed or not SDK-managed)`
423
+ );
424
+ break;
425
+ case "failed":
426
+ warnings.push(
427
+ `Failed to write ${relPath}${discoveryResult.error !== void 0 ? `: ${discoveryResult.error}` : ""}. The Glasstrace browser extension will fall back to the runtime handler until the file is written.`
428
+ );
429
+ break;
430
+ }
431
+ const gitignorePath = path.join(projectRoot, ".gitignore");
432
+ if (fs.existsSync(gitignorePath)) {
433
+ try {
434
+ const gitignoreContent = fs.readFileSync(gitignorePath, "utf-8");
435
+ if (gitignoreExcludesDiscoveryFile(gitignoreContent, discoveryResult.layout)) {
436
+ warnings.push(
437
+ `Your .gitignore excludes ${relPath} (directly or via a parent rule). The discovery file must be committed for the Glasstrace browser extension to find it in deployed builds. Remove the matching line from .gitignore or add an explicit negation (e.g. \`!` + relPath + "`)."
438
+ );
439
+ }
440
+ } catch {
441
+ }
442
+ }
443
+ } catch (err) {
444
+ warnings.push(
445
+ `Failed to write ${relativeDiscoveryPath(
446
+ resolveStaticRoot(projectRoot).layout
447
+ )}: ${err instanceof Error ? err.message : String(err)}`
448
+ );
449
+ }
351
450
  let anyConfigWritten = false;
352
451
  if (isCI) {
353
452
  const genericAgent = {
@@ -509,7 +608,7 @@ async function verifyAnonKeyRegistration(projectRoot) {
509
608
  }
510
609
  const baseConfig = resolveConfig({ apiKey: devKey });
511
610
  const config = { ...baseConfig, apiKey: devKey };
512
- const sdkVersion = true ? "0.19.0" : "0.0.0-dev";
611
+ const sdkVersion = true ? "0.20.0" : "0.0.0-dev";
513
612
  const result = await verifyInitReachable(config, anonKey, sdkVersion);
514
613
  if (result.ok) {
515
614
  return { outcome: "verified" };
@@ -758,6 +857,7 @@ Usage:
758
857
  }
759
858
  export {
760
859
  decideMcpConfigAction,
860
+ gitignoreExcludesDiscoveryFile,
761
861
  meetsNodeVersion,
762
862
  rollbackSteps,
763
863
  runInit,