infinite-tag 0.1.1

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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/dist/src/apply.d.ts +14 -0
  4. package/dist/src/apply.js +84 -0
  5. package/dist/src/cli.d.ts +2 -0
  6. package/dist/src/cli.js +416 -0
  7. package/dist/src/frameworks/index.d.ts +4 -0
  8. package/dist/src/frameworks/index.js +16 -0
  9. package/dist/src/frameworks/managed-files.d.ts +10 -0
  10. package/dist/src/frameworks/managed-files.js +104 -0
  11. package/dist/src/frameworks/next-app-router.d.ts +2 -0
  12. package/dist/src/frameworks/next-app-router.js +187 -0
  13. package/dist/src/frameworks/next-pages-router.d.ts +2 -0
  14. package/dist/src/frameworks/next-pages-router.js +169 -0
  15. package/dist/src/frameworks/shared.d.ts +17 -0
  16. package/dist/src/frameworks/shared.js +136 -0
  17. package/dist/src/frameworks/static-html.d.ts +2 -0
  18. package/dist/src/frameworks/static-html.js +137 -0
  19. package/dist/src/frameworks/vite-react.d.ts +2 -0
  20. package/dist/src/frameworks/vite-react.js +274 -0
  21. package/dist/src/index.d.ts +12 -0
  22. package/dist/src/index.js +11 -0
  23. package/dist/src/inspect.d.ts +10 -0
  24. package/dist/src/inspect.js +150 -0
  25. package/dist/src/manifest.d.ts +10 -0
  26. package/dist/src/manifest.js +83 -0
  27. package/dist/src/package-manager.d.ts +6 -0
  28. package/dist/src/package-manager.js +110 -0
  29. package/dist/src/plan.d.ts +9 -0
  30. package/dist/src/plan.js +97 -0
  31. package/dist/src/providers/ga4.d.ts +2 -0
  32. package/dist/src/providers/ga4.js +73 -0
  33. package/dist/src/providers/index.d.ts +3 -0
  34. package/dist/src/providers/index.js +11 -0
  35. package/dist/src/providers/posthog.d.ts +2 -0
  36. package/dist/src/providers/posthog.js +77 -0
  37. package/dist/src/providers/validate.d.ts +31 -0
  38. package/dist/src/providers/validate.js +110 -0
  39. package/dist/src/providers/x.d.ts +2 -0
  40. package/dist/src/providers/x.js +76 -0
  41. package/dist/src/render.d.ts +25 -0
  42. package/dist/src/render.js +260 -0
  43. package/dist/src/types.d.ts +151 -0
  44. package/dist/src/types.js +8 -0
  45. package/dist/src/uninstall.d.ts +7 -0
  46. package/dist/src/uninstall.js +66 -0
  47. package/dist/src/verify.d.ts +5 -0
  48. package/dist/src/verify.js +50 -0
  49. package/dist/src/workspace-artifacts.d.ts +41 -0
  50. package/dist/src/workspace-artifacts.js +171 -0
  51. package/package.json +27 -0
@@ -0,0 +1,76 @@
1
+ import { jsLiteral, validateXEventTagIds, validateXPixelId } from "./validate.js";
2
+ function frameworkEnvKeys(framework) {
3
+ switch (framework) {
4
+ case "next-app-router":
5
+ case "next-pages-router":
6
+ return ["NEXT_PUBLIC_X_EVENT_TAG_IDS", "NEXT_PUBLIC_X_PIXEL_ID"];
7
+ case "vite-react":
8
+ return ["VITE_X_EVENT_TAG_IDS", "VITE_X_PIXEL_ID"];
9
+ case "static-html":
10
+ return [];
11
+ }
12
+ }
13
+ export const xProviderAdapter = {
14
+ id: "x",
15
+ displayName: "X",
16
+ envKeys(framework) {
17
+ return frameworkEnvKeys(framework);
18
+ },
19
+ plan(framework, artifact) {
20
+ const pixelId = artifact && typeof artifact === "object" && "pixelId" in artifact ? artifact.pixelId : undefined;
21
+ const eventTagIds = artifact && typeof artifact === "object" && "eventTagIds" in artifact
22
+ ? artifact.eventTagIds
23
+ : undefined;
24
+ const blockers = [];
25
+ const pixelError = validateXPixelId(pixelId);
26
+ if (pixelError) {
27
+ blockers.push(pixelError);
28
+ }
29
+ const tagsError = validateXEventTagIds(eventTagIds);
30
+ if (tagsError) {
31
+ blockers.push(tagsError);
32
+ }
33
+ return {
34
+ assumptions: blockers.length === 0
35
+ ? ["X wiring will use only the public pixelId and eventTagIds artifacts."]
36
+ : [],
37
+ blockers,
38
+ instructions: blockers.length === 0
39
+ ? [
40
+ {
41
+ path: frameworkInstructionPath(framework),
42
+ action: framework === "static-html" ? "modify" : "create",
43
+ description: framework === "static-html"
44
+ ? "Inject the X public pixel bootstrap into index.html."
45
+ : "Add the X public pixel bootstrap to the managed analytics module.",
46
+ provider: "x",
47
+ snippet: framework === "static-html"
48
+ ? wrapHtmlSnippet(buildBootstrapSnippet(pixelId, eventTagIds))
49
+ : buildBootstrapSnippet(pixelId, eventTagIds)
50
+ }
51
+ ]
52
+ : []
53
+ };
54
+ }
55
+ };
56
+ function frameworkInstructionPath(framework) {
57
+ switch (framework) {
58
+ case "static-html":
59
+ return "index.html";
60
+ case "vite-react":
61
+ return "src/lib/infinite-analytics.ts";
62
+ case "next-app-router":
63
+ case "next-pages-router":
64
+ return "lib/infinite-analytics.ts";
65
+ }
66
+ }
67
+ function buildBootstrapSnippet(pixelId, eventTagIds) {
68
+ return [
69
+ `window.__INFINITE_X_EVENT_TAG_IDS = ${jsLiteral(eventTagIds)};`,
70
+ "!function(e,t,n,s,u,a){e.twq||(s=e.twq=function(){s.exe?s.exe.apply(s,arguments):s.queue.push(arguments)},s.version='1.1',s.queue=[],u=t.createElement(n),u.async=!0,u.src='https://static.ads-twitter.com/uwt.js',a=t.getElementsByTagName(n)[0],a.parentNode.insertBefore(u,a))}(window,document,'script');",
71
+ `twq('config', ${jsLiteral(pixelId)});`
72
+ ].join("\n");
73
+ }
74
+ function wrapHtmlSnippet(source) {
75
+ return ["<script>", source, "</script>"].join("\n");
76
+ }
@@ -0,0 +1,25 @@
1
+ import type { ApplyResult, InspectResult, InstallPlan, UninstallResult, VerifyResult, WorkspaceInstallArtifacts } from "./types.js";
2
+ export declare function frameworkLabel(framework: string): string;
3
+ /** Short "GA4 · G-XXXX" lines for the providers present in the artifacts. */
4
+ export declare function providerLines(artifacts: WorkspaceInstallArtifacts): string[];
5
+ /** Friendly product names for the providers present, e.g. "Google Analytics and PostHog". */
6
+ export declare function providerNames(artifacts: WorkspaceInstallArtifacts): string;
7
+ /** The "I'll make N changes" block shown before applying (preview). */
8
+ export declare function renderPreview(plan: InstallPlan): string;
9
+ /** The narration + ✅ success + next-steps block shown after a successful apply. */
10
+ export declare function renderApplied(input: {
11
+ inspect: InspectResult;
12
+ plan: InstallPlan;
13
+ apply: ApplyResult;
14
+ verify: VerifyResult;
15
+ }): string;
16
+ /** Shown when no artifacts were found and none were passed — the silent-no-op fix. */
17
+ export declare function renderNoArtifacts(artifactsDir: string): string;
18
+ /** Shown when the repo's framework isn't recognised — with a manual GA4 fallback if we have an ID. */
19
+ export declare function renderUnsupported(plan: InstallPlan): string;
20
+ /** Other refusals (existing analytics, low confidence, etc.) surfaced as plain reasons. */
21
+ export declare function renderBlocked(plan: InstallPlan): string;
22
+ export declare function renderInspect(inspect: InspectResult): string;
23
+ export declare function renderVerify(verify: VerifyResult): string;
24
+ export declare function renderUninstall(result: UninstallResult, dryRun: boolean): string;
25
+ export declare function ga4ManualSnippet(measurementId: string): string[];
@@ -0,0 +1,260 @@
1
+ const HEADER = "Infinite OS · analytics installer";
2
+ const MANIFEST_REL = ".infinite/install.json";
3
+ export function frameworkLabel(framework) {
4
+ switch (framework) {
5
+ case "next-app-router":
6
+ return "Next.js — App Router";
7
+ case "next-pages-router":
8
+ return "Next.js — Pages Router";
9
+ case "vite-react":
10
+ return "Vite + React";
11
+ case "static-html":
12
+ return "static HTML";
13
+ default:
14
+ return framework;
15
+ }
16
+ }
17
+ /** Short "GA4 · G-XXXX" lines for the providers present in the artifacts. */
18
+ export function providerLines(artifacts) {
19
+ const lines = [];
20
+ if (artifacts.ga4?.measurementId) {
21
+ lines.push(`GA4 · ${artifacts.ga4.measurementId}`);
22
+ }
23
+ if (artifacts.posthog && (artifacts.posthog.projectKey || artifacts.posthog.apiHost)) {
24
+ const host = artifacts.posthog.apiHost ? ` (${artifacts.posthog.apiHost})` : "";
25
+ lines.push(`PostHog · ${artifacts.posthog.projectKey || "project"}${host}`);
26
+ }
27
+ if (artifacts.x && (artifacts.x.pixelId || artifacts.x.eventTagIds.length > 0)) {
28
+ lines.push(`X Pixel · ${artifacts.x.pixelId || "pixel"}`);
29
+ }
30
+ return lines;
31
+ }
32
+ /** Friendly product names for the providers present, e.g. "Google Analytics and PostHog". */
33
+ export function providerNames(artifacts) {
34
+ const names = [];
35
+ if (artifacts.ga4) {
36
+ names.push("Google Analytics");
37
+ }
38
+ if (artifacts.posthog) {
39
+ names.push("PostHog");
40
+ }
41
+ if (artifacts.x) {
42
+ names.push("X Pixel");
43
+ }
44
+ return joinWithAnd(names) || "analytics";
45
+ }
46
+ function joinWithAnd(items) {
47
+ if (items.length <= 1) {
48
+ return items[0] ?? "";
49
+ }
50
+ if (items.length === 2) {
51
+ return `${items[0]} and ${items[1]}`;
52
+ }
53
+ return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
54
+ }
55
+ function repoLabel(plan) {
56
+ return plan.appRoot && plan.appRoot !== "." ? `this repo (app at ${plan.appRoot})` : "this repo";
57
+ }
58
+ function actionByPath(plan) {
59
+ const map = new Map();
60
+ for (const instruction of plan.instructions) {
61
+ // "create" only wins when no "modify" already claimed the path.
62
+ if (!map.has(instruction.path) || instruction.action === "modify") {
63
+ map.set(instruction.path, instruction.action);
64
+ }
65
+ }
66
+ return map;
67
+ }
68
+ /** The "I'll make N changes" block shown before applying (preview). */
69
+ export function renderPreview(plan) {
70
+ const actions = actionByPath(plan);
71
+ const tagWord = providerNames(plan.artifacts) === "analytics" ? "analytics" : `${providerNames(plan.artifacts)}`;
72
+ const changeLines = plan.files.map((file) => {
73
+ const symbol = actions.get(file) === "create" ? "+" : "~";
74
+ return ` ${symbol} ${pad(file)}${actions.get(file) === "create" ? "add" : "inject"} the ${tagWord} tag`;
75
+ });
76
+ changeLines.push(` + ${pad(MANIFEST_REL)}install record (lets a later \`uninstall\` undo this cleanly)`);
77
+ const providerBlock = providerLines(plan.artifacts);
78
+ const analyticsValue = providerBlock.length === 0
79
+ ? "—"
80
+ : providerBlock[0] + (providerBlock.length > 1 ? "\n" + providerBlock.slice(1).map((l) => ` ${l}`).join("\n") : "");
81
+ const meta = [` Project repo ${repoLabel(plan)} (${frameworkLabel(plan.framework)})`];
82
+ if (plan.packageManager !== "unknown" && plan.packageManager !== "ambiguous") {
83
+ meta.push(` Package manager ${plan.packageManager}`);
84
+ }
85
+ meta.push(` Analytics ${analyticsValue}`);
86
+ return [
87
+ "",
88
+ HEADER,
89
+ "",
90
+ ...meta,
91
+ "",
92
+ `I'll make ${plan.files.length + 1} change${plan.files.length + 1 === 1 ? "" : "s"}:`,
93
+ ...changeLines,
94
+ ""
95
+ ].join("\n");
96
+ }
97
+ /** The narration + ✅ success + next-steps block shown after a successful apply. */
98
+ export function renderApplied(input) {
99
+ const { plan, apply, verify } = input;
100
+ const names = providerNames(plan.artifacts);
101
+ const codeFiles = apply.changedFiles.filter((file) => !file.endsWith("install.json"));
102
+ const manifestWritten = apply.changedFiles.some((file) => file.endsWith("install.json"));
103
+ const steps = [` ✓ Detected ${frameworkLabel(plan.framework)} at ${plan.appRoot}`];
104
+ if (codeFiles.length > 0) {
105
+ steps.push(` ✓ Installed ${names} → ${codeFiles.join(", ")}`);
106
+ }
107
+ if (manifestWritten) {
108
+ steps.push(` ✓ Recorded the install → ${MANIFEST_REL}`);
109
+ }
110
+ if (verify.buildOk) {
111
+ const n = plan.files.length;
112
+ steps.push(` ✓ Verified ${n} file${n === 1 ? "" : "s"} against the recorded install`);
113
+ }
114
+ const ids = providerLines(plan.artifacts).join(", ");
115
+ const idSuffix = ids ? ` (${ids})` : "";
116
+ const lines = [
117
+ "",
118
+ "Installing analytics into your site…",
119
+ ...steps,
120
+ "",
121
+ `✅ Done — your site is now wired for ${names}${idSuffix}.`,
122
+ "",
123
+ "Next steps:",
124
+ " 1. Review the change: git diff",
125
+ " 2. Commit & deploy your site so the tag goes live.",
126
+ ` 3. Confirm it's working: ${confirmHint(plan.artifacts)}`
127
+ ];
128
+ const warnings = [...apply.warnings, ...verify.warnings.filter((w) => !w.startsWith("Static verification only"))];
129
+ if (warnings.length > 0) {
130
+ lines.push("", "Notes:", ...warnings.map((w) => ` • ${w}`));
131
+ }
132
+ lines.push("");
133
+ return lines.join("\n");
134
+ }
135
+ function confirmHint(artifacts) {
136
+ if (artifacts.ga4) {
137
+ return "open Google Analytics → Realtime (live within minutes; full reports ~24h).";
138
+ }
139
+ if (artifacts.posthog) {
140
+ return "open PostHog → Activity to watch events arrive.";
141
+ }
142
+ return "check your provider's dashboard for incoming events.";
143
+ }
144
+ /** Shown when no artifacts were found and none were passed — the silent-no-op fix. */
145
+ export function renderNoArtifacts(artifactsDir) {
146
+ return [
147
+ "",
148
+ HEADER,
149
+ "",
150
+ "I couldn't find any analytics to install.",
151
+ "",
152
+ `No saved setup data was found on this machine (looked in ${artifactsDir}). Either:`,
153
+ " • Run `infinite setup` first (it saves your keys there), then re-run this; or",
154
+ " • Pass it directly: npx infinite-tag install --ga4-measurement-id G-XXXXXXX",
155
+ ""
156
+ ].join("\n");
157
+ }
158
+ /** Shown when the repo's framework isn't recognised — with a manual GA4 fallback if we have an ID. */
159
+ export function renderUnsupported(plan) {
160
+ const lines = ["", HEADER, ""];
161
+ const providerBlock = providerLines(plan.artifacts);
162
+ if (providerBlock.length > 0) {
163
+ lines.push(` Analytics ${providerBlock.join(", ")}`, "");
164
+ }
165
+ lines.push("⚠️ I couldn't recognize this project's framework (I look for Next.js, Vite + React, or static HTML).", "Nothing was changed.");
166
+ const measurementId = plan.artifacts.ga4?.measurementId;
167
+ if (measurementId) {
168
+ lines.push("", "You can add the Google Analytics tag by hand — paste this into the <head> of every page:", ...ga4ManualSnippet(measurementId));
169
+ }
170
+ lines.push("");
171
+ return lines.join("\n");
172
+ }
173
+ /** Other refusals (existing analytics, low confidence, etc.) surfaced as plain reasons. */
174
+ export function renderBlocked(plan) {
175
+ return [
176
+ "",
177
+ HEADER,
178
+ "",
179
+ "I can't safely install yet:",
180
+ ...plan.blockers.map((blocker) => ` • ${blocker}`),
181
+ "",
182
+ "Fix the above and re-run, or pass --json to see the full machine-readable plan.",
183
+ ""
184
+ ].join("\n");
185
+ }
186
+ export function renderInspect(inspect) {
187
+ const lines = [
188
+ "",
189
+ HEADER,
190
+ "",
191
+ ` Framework ${frameworkLabel(inspect.framework)}`,
192
+ ` App root ${inspect.appRoot}`,
193
+ ` Package manager ${inspect.packageManager}`,
194
+ ` Git status ${inspect.repoStatus}`
195
+ ];
196
+ if (inspect.existingProviders.length > 0) {
197
+ lines.push(` Existing tags ${inspect.existingProviders.join(", ")}`);
198
+ }
199
+ if (inspect.blockers.length > 0) {
200
+ lines.push("", "Blockers:", ...inspect.blockers.map((b) => ` • ${b}`));
201
+ }
202
+ lines.push("");
203
+ return lines.join("\n");
204
+ }
205
+ export function renderVerify(verify) {
206
+ const lines = ["", HEADER, ""];
207
+ if (verify.buildOk) {
208
+ lines.push("✅ Verified — the managed analytics files match the recorded install.");
209
+ }
210
+ else {
211
+ lines.push("❌ Verification failed:");
212
+ lines.push(...verify.routeChecks.map((c) => ` • ${c}`));
213
+ }
214
+ lines.push("");
215
+ return lines.join("\n");
216
+ }
217
+ export function renderUninstall(result, dryRun) {
218
+ const lines = ["", HEADER, ""];
219
+ if (!result.manifestPath) {
220
+ lines.push("Nothing to uninstall — no Infinite install record was found in this repo.", "");
221
+ return lines.join("\n");
222
+ }
223
+ if (dryRun) {
224
+ lines.push("Uninstall preview (nothing was changed):");
225
+ }
226
+ else {
227
+ lines.push("✅ Removed the Infinite analytics install:");
228
+ }
229
+ for (const file of result.restoredFiles) {
230
+ lines.push(` ~ ${file}${dryRun ? " (would restore)" : " restored"}`);
231
+ }
232
+ for (const file of result.removedFiles) {
233
+ lines.push(` - ${file}${dryRun ? " (would remove)" : " removed"}`);
234
+ }
235
+ if (result.warnings.length > 0) {
236
+ lines.push("", "Notes:", ...result.warnings.map((w) => ` • ${w}`));
237
+ }
238
+ if (dryRun) {
239
+ lines.push("", "To remove for real: npx infinite-tag uninstall --yes");
240
+ }
241
+ lines.push("");
242
+ return lines.join("\n");
243
+ }
244
+ export function ga4ManualSnippet(measurementId) {
245
+ return [
246
+ "",
247
+ " <!-- Google tag (gtag.js) -->",
248
+ ` <script async src="https://www.googletagmanager.com/gtag/js?id=${measurementId}"></script>`,
249
+ " <script>",
250
+ " window.dataLayer = window.dataLayer || [];",
251
+ " function gtag(){dataLayer.push(arguments);}",
252
+ " gtag('js', new Date());",
253
+ ` gtag('config', '${measurementId}');`,
254
+ " </script>"
255
+ ];
256
+ }
257
+ function pad(value) {
258
+ const width = 26;
259
+ return value.length >= width ? `${value} ` : value.padEnd(width);
260
+ }
@@ -0,0 +1,151 @@
1
+ export declare const packageManagers: readonly ["pnpm", "npm", "yarn", "bun"];
2
+ export type PackageManager = (typeof packageManagers)[number];
3
+ export type PackageManagerDetectionKind = PackageManager | "ambiguous" | "unknown";
4
+ export type RepoStatus = "clean" | "dirty" | "not-a-git-repo";
5
+ export type ApplyMode = "supported" | "plan-only";
6
+ export declare const supportedFrameworks: readonly ["next-app-router", "next-pages-router", "vite-react", "static-html"];
7
+ export type SupportedFramework = (typeof supportedFrameworks)[number];
8
+ export declare const providerIds: readonly ["ga4", "posthog", "x"];
9
+ export type ProviderId = (typeof providerIds)[number];
10
+ export interface PackageManagerDetection {
11
+ kind: PackageManagerDetectionKind;
12
+ reason: "lockfile" | "multiple-lockfiles" | "no-lockfile" | "override";
13
+ lockfiles: string[];
14
+ }
15
+ export interface PackageManagerCommands {
16
+ packageManager: PackageManager;
17
+ oneOff: string;
18
+ repeatableInstall: string;
19
+ repeatableRun: string;
20
+ }
21
+ export interface InspectResult {
22
+ framework: string;
23
+ appRoot: string;
24
+ packageManager: string;
25
+ confidence: number;
26
+ existingProviders: string[];
27
+ repoStatus: RepoStatus;
28
+ assumptions: string[];
29
+ blockers: string[];
30
+ detectedFiles: string[];
31
+ }
32
+ export interface InstallPlan {
33
+ framework: string;
34
+ providers: string[];
35
+ files: string[];
36
+ envKeys: string[];
37
+ applyMode: ApplyMode;
38
+ instructions: InstallInstruction[];
39
+ assumptions: string[];
40
+ blockers: string[];
41
+ confidence: number;
42
+ appRoot: string;
43
+ packageManager: string;
44
+ repoStatus: RepoStatus;
45
+ workspaceId?: string;
46
+ artifacts: WorkspaceInstallArtifacts;
47
+ }
48
+ export interface ApplyResult {
49
+ changedFiles: string[];
50
+ manifestPath: string;
51
+ warnings: string[];
52
+ }
53
+ export interface UninstallResult {
54
+ removedFiles: string[];
55
+ restoredFiles: string[];
56
+ warnings: string[];
57
+ manifestPath: string | null;
58
+ }
59
+ export interface VerifyResult {
60
+ buildOk: boolean;
61
+ routeChecks: string[];
62
+ beaconChecks: string[];
63
+ warnings: string[];
64
+ }
65
+ export interface Ga4PublicArtifact {
66
+ measurementId: string;
67
+ }
68
+ export interface PosthogPublicArtifact {
69
+ projectKey: string;
70
+ apiHost: string;
71
+ }
72
+ export interface XPublicArtifact {
73
+ pixelId: string;
74
+ eventTagIds: string[];
75
+ }
76
+ export interface WorkspaceInstallArtifacts {
77
+ ga4?: Ga4PublicArtifact;
78
+ posthog?: PosthogPublicArtifact;
79
+ x?: XPublicArtifact;
80
+ }
81
+ export interface InstallManifest {
82
+ workspaceId: string;
83
+ appRoot: string;
84
+ framework: SupportedFramework;
85
+ providers: ProviderId[];
86
+ files: string[];
87
+ envKeys: string[];
88
+ contentHashes: Record<string, string>;
89
+ wiringVersion: number;
90
+ verifiedAt: string | null;
91
+ }
92
+ export interface FrameworkMatch {
93
+ framework: SupportedFramework;
94
+ confidence: number;
95
+ files: string[];
96
+ assumptions: string[];
97
+ }
98
+ export interface FrameworkPlanDraft {
99
+ files: string[];
100
+ applyMode: ApplyMode;
101
+ instructions: InstallInstruction[];
102
+ assumptions: string[];
103
+ blockers: string[];
104
+ confidence: number;
105
+ }
106
+ export interface InstallInstruction {
107
+ path: string;
108
+ action: "create" | "modify";
109
+ description: string;
110
+ snippet: string;
111
+ provider?: ProviderId;
112
+ }
113
+ export interface FrameworkAdapter {
114
+ id: SupportedFramework;
115
+ displayName: string;
116
+ detect(root: string): FrameworkMatch | null;
117
+ plan(root: string): FrameworkPlanDraft;
118
+ apply?(context: FrameworkApplyContext): FrameworkApplyResult;
119
+ uninstall?(context: FrameworkUninstallContext): FrameworkUninstallResult;
120
+ }
121
+ export interface ProviderPlanDraft {
122
+ assumptions: string[];
123
+ blockers: string[];
124
+ instructions: InstallInstruction[];
125
+ }
126
+ export interface FrameworkApplyContext {
127
+ root: string;
128
+ appRoot: string;
129
+ plan: InstallPlan;
130
+ }
131
+ export interface FrameworkApplyResult {
132
+ changedFiles: string[];
133
+ warnings: string[];
134
+ }
135
+ export interface FrameworkUninstallContext {
136
+ root: string;
137
+ appRoot: string;
138
+ manifest: InstallManifest;
139
+ dryRun: boolean;
140
+ }
141
+ export interface FrameworkUninstallResult {
142
+ removedFiles: string[];
143
+ restoredFiles: string[];
144
+ warnings: string[];
145
+ }
146
+ export interface ProviderAdapter {
147
+ id: ProviderId;
148
+ displayName: string;
149
+ envKeys(framework: SupportedFramework): string[];
150
+ plan(framework: SupportedFramework, artifact: WorkspaceInstallArtifacts[ProviderId] | undefined): ProviderPlanDraft;
151
+ }
@@ -0,0 +1,8 @@
1
+ export const packageManagers = ["pnpm", "npm", "yarn", "bun"];
2
+ export const supportedFrameworks = [
3
+ "next-app-router",
4
+ "next-pages-router",
5
+ "vite-react",
6
+ "static-html"
7
+ ];
8
+ export const providerIds = ["ga4", "posthog", "x"];
@@ -0,0 +1,7 @@
1
+ import type { UninstallResult } from "./types.js";
2
+ export interface UninstallInstallationOptions {
3
+ root: string;
4
+ allowDirty?: boolean;
5
+ dryRun?: boolean;
6
+ }
7
+ export declare function uninstallInstallation(options: UninstallInstallationOptions): UninstallResult;
@@ -0,0 +1,66 @@
1
+ import { existsSync, readdirSync, rmdirSync, rmSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { snapshotFiles, restoreSnapshot } from "./apply.js";
4
+ import { getFrameworkAdapter } from "./frameworks/index.js";
5
+ import { detectRepoStatus } from "./inspect.js";
6
+ import { installManifestPath, installManifestRelativePath, readInstallManifest } from "./manifest.js";
7
+ export function uninstallInstallation(options) {
8
+ const manifest = readInstallManifest(options.root);
9
+ if (!manifest) {
10
+ return {
11
+ removedFiles: [],
12
+ restoredFiles: [],
13
+ warnings: ["No .infinite/install.json manifest found. Nothing to uninstall."],
14
+ manifestPath: null
15
+ };
16
+ }
17
+ const dryRun = options.dryRun ?? false;
18
+ if (!dryRun && detectRepoStatus(options.root) === "dirty" && !options.allowDirty) {
19
+ throw new Error("Refusing to uninstall on a dirty git tree without --allow-dirty.");
20
+ }
21
+ const adapter = getFrameworkAdapter(manifest.framework);
22
+ if (!adapter?.uninstall) {
23
+ throw new Error(`No uninstall implementation is registered for ${manifest.framework}.`);
24
+ }
25
+ const snapshot = snapshotFiles(options.root, [
26
+ ...manifest.files,
27
+ installManifestRelativePath
28
+ ]);
29
+ let frameworkResult;
30
+ try {
31
+ frameworkResult = adapter.uninstall({
32
+ root: options.root,
33
+ appRoot: manifest.appRoot,
34
+ manifest,
35
+ dryRun
36
+ });
37
+ }
38
+ catch (error) {
39
+ restoreSnapshot(options.root, snapshot);
40
+ throw error;
41
+ }
42
+ const hasWiringLeftover = frameworkResult.warnings.some((w) => w.includes("automatically") || w.includes("leftover"));
43
+ const manifestPath = installManifestPath(options.root);
44
+ if (!dryRun && !hasWiringLeftover) {
45
+ rmSync(manifestPath);
46
+ removeDirIfEmpty(dirname(manifestPath));
47
+ // Also prune empty lib dirs left by adapter file removals
48
+ const appRoot = manifest.appRoot === "." ? options.root : join(options.root, manifest.appRoot);
49
+ for (const candidate of ["lib", "src/lib"]) {
50
+ removeDirIfEmpty(join(appRoot, candidate));
51
+ }
52
+ }
53
+ return {
54
+ removedFiles: hasWiringLeftover
55
+ ? frameworkResult.removedFiles
56
+ : [...frameworkResult.removedFiles, installManifestRelativePath],
57
+ restoredFiles: frameworkResult.restoredFiles,
58
+ warnings: frameworkResult.warnings,
59
+ manifestPath: hasWiringLeftover ? null : manifestPath
60
+ };
61
+ }
62
+ function removeDirIfEmpty(path) {
63
+ if (existsSync(path) && readdirSync(path).length === 0) {
64
+ rmdirSync(path);
65
+ }
66
+ }
@@ -0,0 +1,5 @@
1
+ import type { VerifyResult } from "./types.js";
2
+ export interface VerifyInstallationOptions {
3
+ root: string;
4
+ }
5
+ export declare function verifyInstallation(options: VerifyInstallationOptions): VerifyResult;
@@ -0,0 +1,50 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { readInstallManifest } from "./manifest.js";
5
+ export function verifyInstallation(options) {
6
+ const manifest = readInstallManifest(options.root);
7
+ if (!manifest) {
8
+ return {
9
+ buildOk: false,
10
+ routeChecks: ["Missing .infinite/install.json"],
11
+ beaconChecks: [],
12
+ warnings: ["Run apply before verify so the manifest exists."]
13
+ };
14
+ }
15
+ const routeChecks = [`Manifest loaded for ${manifest.framework} at ${manifest.appRoot}.`];
16
+ const failures = [];
17
+ let verifiedFileCount = 0;
18
+ for (const relativePath of manifest.files) {
19
+ const absolutePath = join(options.root, relativePath);
20
+ if (!existsSync(absolutePath)) {
21
+ failures.push(`Missing managed file: ${relativePath}`);
22
+ continue;
23
+ }
24
+ const expectedHash = manifest.contentHashes[relativePath];
25
+ if (!expectedHash) {
26
+ failures.push(`Manifest is missing a content hash for ${relativePath}`);
27
+ continue;
28
+ }
29
+ const actualHash = createHash("sha256").update(readFileSync(absolutePath)).digest("hex");
30
+ if (actualHash !== expectedHash) {
31
+ failures.push(`Managed file content drifted from manifest: ${relativePath}`);
32
+ continue;
33
+ }
34
+ verifiedFileCount += 1;
35
+ }
36
+ if (failures.length === 0) {
37
+ routeChecks.push(`Verified ${verifiedFileCount} managed file${verifiedFileCount === 1 ? "" : "s"} against recorded content hashes.`);
38
+ }
39
+ else {
40
+ routeChecks.push(...failures);
41
+ }
42
+ return {
43
+ buildOk: failures.length === 0,
44
+ routeChecks,
45
+ beaconChecks: manifest.providers.map((provider) => `${provider}: manifest-backed wiring is present in the managed install files.`),
46
+ warnings: [
47
+ "Static verification only. Runtime beacon delivery still requires a browser/network check."
48
+ ]
49
+ };
50
+ }