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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Ultima AI, Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # infinite-tag
2
+
3
+ Add analytics to your web app with one command. `infinite-tag` installs
4
+ **Google Analytics 4**, **PostHog**, and the **X (Twitter) Pixel** into your
5
+ codebase — using only your **public** keys, with changes that are idempotent and
6
+ fully reversible.
7
+
8
+ You run it **inside your own web app's repository**. It detects your framework,
9
+ writes a small managed analytics module + the wiring to load it, and records a
10
+ manifest so it can cleanly uninstall later. It never asks for — or stores — any
11
+ secret.
12
+
13
+ ---
14
+
15
+ ## Quick start
16
+
17
+ Run it in the root of your web app's repo:
18
+
19
+ ```bash
20
+ # Preview what would change (no files written):
21
+ npx infinite-tag@latest install --ga4-measurement-id G-XXXXXXXXXX
22
+
23
+ # Apply it (writes the files):
24
+ npx infinite-tag@latest install \
25
+ --workspace <your-infinite-workspace-id> \
26
+ --ga4-measurement-id G-XXXXXXXXXX \
27
+ --posthog-project-key phc_xxxxxxxxxxxxxxxx \
28
+ --posthog-api-host https://us.i.posthog.com \
29
+ --yes
30
+ ```
31
+
32
+ > **The easy path:** run `infinite setup` in the Infinite app — once your
33
+ > analytics are connected it prints a ready-to-paste `npx infinite-tag install …`
34
+ > command with your keys and workspace id already filled in. Copy it, run it in
35
+ > your repo, done.
36
+ >
37
+ > **Even easier on the same machine:** `infinite setup` also saves your public
38
+ > keys to `~/.infinite/artifacts/<workspace>.json`, so a bare
39
+ > `npx infinite-tag@latest install` run in your repo discovers them automatically —
40
+ > nothing to paste. Explicit flags or `--artifact-file` always take precedence,
41
+ > and with several saved workspaces you pass `--workspace <id>` to pick one
42
+ > (it never guesses). Only public keys are ever stored in that file.
43
+
44
+ Without `--yes`, every command is a **dry run** that shows the plan and writes
45
+ nothing. Applying requires `--yes` **and** `--workspace <id>`.
46
+
47
+ ---
48
+
49
+ ## Commands
50
+
51
+ | Command | What it does |
52
+ | --- | --- |
53
+ | `inspect` | Detect your framework, package manager, and current analytics state. Writes nothing. |
54
+ | `plan` | Show exactly which files would be created/modified, and any blockers. Writes nothing. |
55
+ | `install` | Plan → (with `--yes`) apply → static verification. The main command. |
56
+ | `apply` | Just apply a plan (requires `--yes` and `--workspace`). |
57
+ | `verify` | Static verification of the managed files against the manifest's recorded sha256 hashes; no build is run. |
58
+ | `uninstall` | Remove everything `infinite-tag` installed, restoring your files. Dry run unless `--yes`. |
59
+ | `help` | Usage. |
60
+
61
+ > Note: the `buildOk` field in `--json` output reflects these static checks
62
+ > only (the name is kept for compatibility); no build is executed.
63
+
64
+ ## Options
65
+
66
+ | Flag | Description |
67
+ | --- | --- |
68
+ | `--ga4-measurement-id <G-…>` | Public GA4 / gtag measurement ID. |
69
+ | `--posthog-project-key <phc_…>` | Public PostHog project key. |
70
+ | `--posthog-api-host <https://…>` | PostHog ingestion host (e.g. `https://us.i.posthog.com`; reverse-proxy paths are preserved). |
71
+ | `--x-pixel-id <id>` | Public X/Twitter Pixel ID. |
72
+ | `--x-event-tag-id <id>` | X event tag ID (repeatable). |
73
+ | `--artifact-file <path>` | Read the public artifacts above from a JSON file instead of flags. |
74
+ | `--workspace <id>` | Your Infinite workspace id (recorded in the manifest). Required to apply. |
75
+ | `--app-root <path>` | App directory, if it isn't the repo root (monorepos). |
76
+ | `--package-manager <pnpm\|npm\|yarn\|bun>` | Override package-manager detection. |
77
+ | `--yes` | Actually write changes (otherwise dry run). |
78
+ | `--allow-dirty` | Proceed even if the git tree has uncommitted changes. |
79
+ | `--json` | Machine-readable output. |
80
+
81
+ Only **public** values are ever accepted. Private/server keys (e.g. a PostHog
82
+ *personal* API key) are never passed to this tool.
83
+
84
+ ---
85
+
86
+ ## Supported frameworks
87
+
88
+ - **Next.js** — App Router and Pages Router
89
+ - **Vite + React**
90
+ - **Static HTML** (a plain `index.html` site)
91
+
92
+ If your repo can't be confidently classified, `infinite-tag` stops and tells you,
93
+ rather than guessing.
94
+
95
+ ## What it writes to your repo
96
+
97
+ - A managed analytics module (e.g. `lib/infinite-analytics.ts`) plus the minimal
98
+ framework wiring to load it. Static-HTML sites get an
99
+ `<!-- infinite:start --> … <!-- infinite:end -->` block in `index.html`.
100
+ - A manifest at `.infinite/install.json` recording every managed file with a
101
+ content hash, so `uninstall` can verify and reverse the change.
102
+
103
+ Every managed file is stamped `// Managed by Infinite` so the tool can recognize
104
+ its own work.
105
+
106
+ ## Uninstall
107
+
108
+ ```bash
109
+ # Preview the removal:
110
+ npx infinite-tag@latest uninstall
111
+
112
+ # Actually remove it:
113
+ npx infinite-tag@latest uninstall --yes
114
+ ```
115
+
116
+ Uninstall restores your files to their pre-install state byte-for-byte. If you
117
+ hand-edited a managed file, `infinite-tag` refuses to delete it (so your edits are
118
+ never lost) and tells you what to remove manually.
119
+
120
+ ---
121
+
122
+ ## Safety
123
+
124
+ - **Public keys only.** No secrets are accepted, requested, or stored.
125
+ - **Idempotent.** Running `install` twice does not duplicate the wiring.
126
+ - **Reversible.** `uninstall` cleanly restores your files; applies are written
127
+ atomically and roll back on failure.
128
+ - **No clobbering.** It refuses to overwrite an existing, unmanaged analytics tag
129
+ or a file it doesn't recognize as its own.
130
+ - **Stays in your repo.** It never writes outside the app root (no `..`, no
131
+ absolute paths, no symlink escapes).
132
+ - **Git-aware.** It won't apply or uninstall on a dirty tree unless you pass
133
+ `--allow-dirty`.
134
+
135
+ ## Community
136
+
137
+ - Discord: <https://discord.gg/F2CT4C7R>
138
+ - X: [@infiniteOS_](https://x.com/infiniteOS_) — built by [@RiverKhan](https://x.com/RiverKhan)
139
+
140
+ ## License
141
+
142
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,14 @@
1
+ import type { ApplyResult, InstallPlan } from "./types.js";
2
+ export interface ApplyInstallationOptions {
3
+ root: string;
4
+ workspaceId: string;
5
+ plan: InstallPlan;
6
+ allowDirty?: boolean;
7
+ }
8
+ export declare function applyInstallation(options: ApplyInstallationOptions): ApplyResult;
9
+ export interface FileSnapshot {
10
+ relativePath: string;
11
+ contents: string | null;
12
+ }
13
+ export declare function snapshotFiles(root: string, relativePaths: string[]): FileSnapshot[];
14
+ export declare function restoreSnapshot(root: string, snapshot: FileSnapshot[]): void;
@@ -0,0 +1,84 @@
1
+ import { existsSync, readFileSync, rmSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { writeFileAtomic } from "./frameworks/shared.js";
4
+ import { getFrameworkAdapter } from "./frameworks/index.js";
5
+ import { computeContentHashes, installManifestRelativePath, writeInstallManifestIfChanged } from "./manifest.js";
6
+ const minimumApplyConfidence = 0.75;
7
+ export function applyInstallation(options) {
8
+ if (options.plan.blockers.length > 0) {
9
+ throw new Error(`Refusing to apply an unsupported or blocked plan: ${options.plan.blockers.join(" ")}`);
10
+ }
11
+ if (options.plan.confidence < minimumApplyConfidence) {
12
+ throw new Error(`Refusing to apply a low-confidence plan (${options.plan.confidence.toFixed(2)}).`);
13
+ }
14
+ if (options.plan.repoStatus === "dirty" && !options.allowDirty) {
15
+ throw new Error("Refusing to apply on a dirty git tree without --allow-dirty.");
16
+ }
17
+ if (options.plan.applyMode !== "supported") {
18
+ throw new Error(`Refusing to apply a plan-only framework (${options.plan.framework}). Review the plan instructions and wire it manually for now.`);
19
+ }
20
+ const frameworkAdapter = getFrameworkAdapter(options.plan.framework);
21
+ if (!frameworkAdapter?.apply) {
22
+ throw new Error(`No apply implementation is registered for ${options.plan.framework}.`);
23
+ }
24
+ const snapshot = snapshotFiles(options.root, [
25
+ ...options.plan.files,
26
+ installManifestRelativePath
27
+ ]);
28
+ try {
29
+ const frameworkResult = frameworkAdapter.apply({
30
+ root: options.root,
31
+ appRoot: options.plan.appRoot,
32
+ plan: options.plan
33
+ });
34
+ const manifest = {
35
+ workspaceId: options.workspaceId,
36
+ appRoot: options.plan.appRoot,
37
+ framework: options.plan.framework,
38
+ providers: options.plan.providers,
39
+ files: options.plan.files,
40
+ envKeys: options.plan.envKeys,
41
+ contentHashes: computeContentHashes(options.root, options.plan.files),
42
+ wiringVersion: 1,
43
+ verifiedAt: null
44
+ };
45
+ const manifestWrite = writeInstallManifestIfChanged(options.root, manifest);
46
+ const changedFiles = [...frameworkResult.changedFiles];
47
+ if (manifestWrite.changed) {
48
+ changedFiles.push(relative(options.root, manifestWrite.manifestPath) || ".infinite/install.json");
49
+ }
50
+ return {
51
+ changedFiles,
52
+ manifestPath: manifestWrite.manifestPath,
53
+ warnings: frameworkResult.warnings
54
+ };
55
+ }
56
+ catch (error) {
57
+ restoreSnapshot(options.root, snapshot);
58
+ throw error;
59
+ }
60
+ }
61
+ export function snapshotFiles(root, relativePaths) {
62
+ return relativePaths.map((relativePath) => {
63
+ const absolutePath = join(root, relativePath);
64
+ return {
65
+ relativePath,
66
+ contents: existsSync(absolutePath) ? readFileSync(absolutePath, "utf8") : null
67
+ };
68
+ });
69
+ }
70
+ export function restoreSnapshot(root, snapshot) {
71
+ for (const file of snapshot) {
72
+ const absolutePath = join(root, file.relativePath);
73
+ const current = existsSync(absolutePath) ? readFileSync(absolutePath, "utf8") : null;
74
+ if (current === file.contents) {
75
+ continue;
76
+ }
77
+ if (file.contents === null) {
78
+ rmSync(absolutePath, { force: true });
79
+ }
80
+ else {
81
+ writeFileAtomic(absolutePath, file.contents);
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function runCli(argv?: string[]): Promise<number>;
@@ -0,0 +1,416 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from "node:readline";
3
+ import { resolve } from "node:path";
4
+ import { applyInstallation } from "./apply.js";
5
+ import { isSupportedFramework } from "./frameworks/index.js";
6
+ import { inspectWorkspace } from "./inspect.js";
7
+ import { buildPackageManagerCommands } from "./package-manager.js";
8
+ import { planInstallation } from "./plan.js";
9
+ import { renderApplied, renderBlocked, renderInspect, renderNoArtifacts, renderPreview, renderUninstall, renderUnsupported, renderVerify } from "./render.js";
10
+ import { uninstallInstallation } from "./uninstall.js";
11
+ import { defaultArtifactsDir, discoverWorkspaceArtifacts, resolveWorkspaceArtifacts } from "./workspace-artifacts.js";
12
+ import { verifyInstallation } from "./verify.js";
13
+ const NO_ARTIFACTS_BLOCKER = "No supported public install artifacts were provided.";
14
+ function parsePackageManager(value) {
15
+ if (value === "pnpm" || value === "npm" || value === "yarn" || value === "bun") {
16
+ return value;
17
+ }
18
+ throw new Error(`Unsupported package manager override: ${value}`);
19
+ }
20
+ function requireValue(flag, value) {
21
+ if (value === undefined || value.startsWith("--")) {
22
+ throw new Error(`Missing value for ${flag}.${value !== undefined ? ` (got "${value}" — values beginning with -- are treated as a missing value)` : ""}`);
23
+ }
24
+ return value;
25
+ }
26
+ function parseArgs(argv) {
27
+ const parsed = {
28
+ command: argv[0] ?? "help",
29
+ json: false,
30
+ yes: false,
31
+ allowDirty: false,
32
+ xEventTagIds: []
33
+ };
34
+ for (let index = 1; index < argv.length; index += 1) {
35
+ const token = argv[index];
36
+ const next = argv[index + 1];
37
+ switch (token) {
38
+ case "--root":
39
+ parsed.root = requireValue(token, next);
40
+ index += 1;
41
+ break;
42
+ case "--workspace":
43
+ parsed.workspaceId = requireValue(token, next);
44
+ index += 1;
45
+ break;
46
+ case "--app-root":
47
+ parsed.appRoot = requireValue(token, next);
48
+ index += 1;
49
+ break;
50
+ case "--artifact-file":
51
+ parsed.artifactFile = requireValue(token, next);
52
+ index += 1;
53
+ break;
54
+ case "--ga4-measurement-id":
55
+ parsed.ga4MeasurementId = requireValue(token, next);
56
+ index += 1;
57
+ break;
58
+ case "--posthog-project-key":
59
+ parsed.posthogProjectKey = requireValue(token, next);
60
+ index += 1;
61
+ break;
62
+ case "--posthog-api-host":
63
+ parsed.posthogApiHost = requireValue(token, next);
64
+ index += 1;
65
+ break;
66
+ case "--x-pixel-id":
67
+ parsed.xPixelId = requireValue(token, next);
68
+ index += 1;
69
+ break;
70
+ case "--x-event-tag-id":
71
+ parsed.xEventTagIds.push(requireValue(token, next));
72
+ index += 1;
73
+ break;
74
+ case "--package-manager":
75
+ parsed.packageManager = parsePackageManager(requireValue(token, next));
76
+ index += 1;
77
+ break;
78
+ case "--json":
79
+ parsed.json = true;
80
+ break;
81
+ case "--yes":
82
+ parsed.yes = true;
83
+ break;
84
+ case "--allow-dirty":
85
+ parsed.allowDirty = true;
86
+ break;
87
+ default:
88
+ throw new Error(`Unknown argument: ${token}. Run infinite-tag help for usage.`);
89
+ }
90
+ }
91
+ return parsed;
92
+ }
93
+ function printResult(_parsed, value) {
94
+ console.log(JSON.stringify(value, null, 2));
95
+ }
96
+ function printHelp() {
97
+ console.log([
98
+ "Usage: infinite-tag <inspect|plan|apply|verify|install|uninstall> [options]",
99
+ "",
100
+ "Commands:",
101
+ " inspect Detect framework, app root, package manager, and existing providers",
102
+ " plan Produce a deterministic install plan from public provider artifacts",
103
+ " apply Apply the plan to your repo and record .infinite/install.json",
104
+ " verify Verify managed analytics files match the recorded manifest",
105
+ " install Inspect -> plan -> (confirm) -> apply -> verify",
106
+ " uninstall Remove the managed install recorded in .infinite/install.json",
107
+ " (dry run without --yes; destructive with --yes)",
108
+ "",
109
+ "Common flags:",
110
+ " --root <path>",
111
+ " --yes Apply without the interactive confirmation",
112
+ " --allow-dirty Skip the clean-git-tree safety gate",
113
+ " --json Output machine-readable JSON instead of human text",
114
+ "",
115
+ "Artifact flags:",
116
+ " --ga4-measurement-id <id>",
117
+ " --posthog-project-key <key>",
118
+ " --posthog-api-host <host>",
119
+ " --x-pixel-id <id>",
120
+ " --x-event-tag-id <id> (repeatable)",
121
+ " --artifact-file <path>",
122
+ "",
123
+ "When no artifact flags and no --artifact-file are given, plan/apply/install",
124
+ "auto-discover the file `infinite setup` saved under ~/.infinite/artifacts/",
125
+ "(<workspace>.json with --workspace; a single saved file otherwise, adopting",
126
+ "its workspace id). Explicit flags always win."
127
+ ].join("\n"));
128
+ }
129
+ function detectedManagerFromInspect(packageManager) {
130
+ if (packageManager === "pnpm" ||
131
+ packageManager === "npm" ||
132
+ packageManager === "yarn" ||
133
+ packageManager === "bun") {
134
+ return packageManager;
135
+ }
136
+ return undefined;
137
+ }
138
+ function maybePrintCommands(command, packageManager, workspaceId) {
139
+ const resolved = detectedManagerFromInspect(packageManager);
140
+ if (!resolved || !workspaceId) {
141
+ return;
142
+ }
143
+ const commands = buildPackageManagerCommands(resolved, {
144
+ pinnedVersion: "0.1.1",
145
+ workspaceId
146
+ });
147
+ if (command === "inspect" || command === "verify") {
148
+ return;
149
+ }
150
+ console.error(`Suggested one-off command: ${commands.oneOff}`);
151
+ }
152
+ /** Classifies why a plan can't be applied, so human mode can show the right guidance. */
153
+ function planIssue(plan) {
154
+ if (!isSupportedFramework(plan.framework)) {
155
+ return "unsupported";
156
+ }
157
+ if (plan.blockers.includes(NO_ARTIFACTS_BLOCKER)) {
158
+ return "no-artifacts";
159
+ }
160
+ if (plan.blockers.length > 0) {
161
+ return "blocked";
162
+ }
163
+ return null;
164
+ }
165
+ /** Renders the appropriate "can't install" message for human mode; null when the plan is clean. */
166
+ function renderPlanIssue(plan) {
167
+ switch (planIssue(plan)) {
168
+ case "unsupported":
169
+ return renderUnsupported(plan);
170
+ case "no-artifacts":
171
+ return renderNoArtifacts(defaultArtifactsDir());
172
+ case "blocked":
173
+ return renderBlocked(plan);
174
+ default:
175
+ return null;
176
+ }
177
+ }
178
+ /** Interactive [Y/n] confirmation on stderr, defaulting to yes. Only call in a TTY. */
179
+ async function confirmApply() {
180
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
181
+ try {
182
+ const answer = await new Promise((resolveAnswer) => {
183
+ rl.question("Apply these changes? [Y/n] ", resolveAnswer);
184
+ });
185
+ const normalized = answer.trim().toLowerCase();
186
+ return normalized === "" || normalized === "y" || normalized === "yes";
187
+ }
188
+ finally {
189
+ rl.close();
190
+ }
191
+ }
192
+ /** Applies + verifies, then prints the human narration/success block. Returns the exit code. */
193
+ function applyAndRenderHuman(ctx) {
194
+ if (ctx.plan.repoStatus === "dirty" && !ctx.allowDirty) {
195
+ console.log("\nYour git tree has uncommitted changes. Commit or stash them first so you can review" +
196
+ "\nexactly what Infinite adds — or re-run with --allow-dirty to proceed anyway.\n");
197
+ return 1;
198
+ }
199
+ const applyResult = applyInstallation({
200
+ root: ctx.root,
201
+ workspaceId: ctx.plan.workspaceId,
202
+ plan: ctx.plan,
203
+ allowDirty: ctx.allowDirty
204
+ });
205
+ const verifyResult = verifyInstallation({ root: ctx.root });
206
+ console.log(renderApplied({ inspect: ctx.inspect, plan: ctx.plan, apply: applyResult, verify: verifyResult }));
207
+ return verifyResult.buildOk ? 0 : 1;
208
+ }
209
+ export async function runCli(argv = process.argv.slice(2)) {
210
+ try {
211
+ const parsed = parseArgs(argv);
212
+ if (parsed.command === "help" || parsed.command === "--help" || parsed.command === "-h") {
213
+ printHelp();
214
+ return 0;
215
+ }
216
+ const root = resolve(parsed.root ?? process.cwd());
217
+ if (parsed.command === "uninstall") {
218
+ const result = uninstallInstallation({
219
+ root,
220
+ allowDirty: parsed.allowDirty,
221
+ dryRun: !parsed.yes
222
+ });
223
+ if (parsed.json) {
224
+ printResult(parsed, result);
225
+ }
226
+ else {
227
+ console.log(renderUninstall(result, !parsed.yes));
228
+ }
229
+ if (!parsed.yes) {
230
+ console.error("Dry run only. Re-run uninstall with --yes to remove the managed install.");
231
+ }
232
+ return 0;
233
+ }
234
+ const inspect = inspectWorkspace(root, {
235
+ appRoot: parsed.appRoot,
236
+ packageManager: parsed.packageManager
237
+ });
238
+ let artifacts = resolveWorkspaceArtifacts(root, {
239
+ artifactFile: parsed.artifactFile,
240
+ ga4MeasurementId: parsed.ga4MeasurementId,
241
+ posthogProjectKey: parsed.posthogProjectKey,
242
+ posthogApiHost: parsed.posthogApiHost,
243
+ xPixelId: parsed.xPixelId,
244
+ xEventTagIds: parsed.xEventTagIds
245
+ });
246
+ // Same-machine flag-free install: with no artifact flags and no --artifact-file,
247
+ // fall back to the public artifacts `infinite setup` saved on this machine.
248
+ // Explicit artifact input always wins, and a workspace id adopted from the
249
+ // discovered file satisfies the `install --yes` workspace requirement.
250
+ const hasExplicitArtifacts = parsed.artifactFile !== undefined ||
251
+ parsed.ga4MeasurementId !== undefined ||
252
+ parsed.posthogProjectKey !== undefined ||
253
+ parsed.posthogApiHost !== undefined ||
254
+ parsed.xPixelId !== undefined ||
255
+ parsed.xEventTagIds.length > 0;
256
+ const commandUsesArtifacts = parsed.command === "plan" || parsed.command === "apply" || parsed.command === "install";
257
+ if (commandUsesArtifacts && !hasExplicitArtifacts) {
258
+ const discovered = discoverWorkspaceArtifacts({
259
+ workspaceId: parsed.workspaceId,
260
+ warn: (message) => console.error(message)
261
+ });
262
+ if (discovered) {
263
+ artifacts = discovered.artifacts;
264
+ const adoptedWorkspaceId = parsed.workspaceId === undefined ? discovered.workspaceId : undefined;
265
+ if (adoptedWorkspaceId !== undefined) {
266
+ parsed.workspaceId = adoptedWorkspaceId;
267
+ }
268
+ console.error(`Discovered saved public artifacts: ${discovered.filePath} (providers: ${discovered.providers.join(", ")}${adoptedWorkspaceId !== undefined ? `; workspace: ${adoptedWorkspaceId}` : ""})`);
269
+ }
270
+ }
271
+ switch (parsed.command) {
272
+ case "inspect":
273
+ if (parsed.json) {
274
+ printResult(parsed, inspect);
275
+ }
276
+ else {
277
+ console.log(renderInspect(inspect));
278
+ }
279
+ return 0;
280
+ case "plan": {
281
+ const plan = planInstallation({
282
+ root,
283
+ inspect,
284
+ workspaceId: parsed.workspaceId,
285
+ packageManager: parsed.packageManager,
286
+ artifacts
287
+ });
288
+ if (parsed.json) {
289
+ printResult(parsed, plan);
290
+ maybePrintCommands(parsed.command, inspect.packageManager, parsed.workspaceId);
291
+ return plan.blockers.length === 0 ? 0 : 1;
292
+ }
293
+ const issue = renderPlanIssue(plan);
294
+ if (issue) {
295
+ console.log(issue);
296
+ return plan.blockers.length === 0 ? 0 : 1;
297
+ }
298
+ console.log(renderPreview(plan));
299
+ console.log("This was a preview — nothing changed. To apply: npx infinite-tag install --yes\n");
300
+ return 0;
301
+ }
302
+ case "apply": {
303
+ if (!parsed.yes) {
304
+ throw new Error("Founder approval is required. Re-run apply with --yes to continue.");
305
+ }
306
+ if (!parsed.workspaceId) {
307
+ throw new Error("apply requires --workspace <workspace-id>.");
308
+ }
309
+ const plan = planInstallation({
310
+ root,
311
+ inspect,
312
+ workspaceId: parsed.workspaceId,
313
+ packageManager: parsed.packageManager,
314
+ artifacts
315
+ });
316
+ if (parsed.json) {
317
+ const result = applyInstallation({
318
+ root,
319
+ workspaceId: parsed.workspaceId,
320
+ plan,
321
+ allowDirty: parsed.allowDirty
322
+ });
323
+ printResult(parsed, result);
324
+ return 0;
325
+ }
326
+ const issue = renderPlanIssue(plan);
327
+ if (issue) {
328
+ console.log(issue);
329
+ return 1;
330
+ }
331
+ return applyAndRenderHuman({ root, inspect, plan, allowDirty: parsed.allowDirty });
332
+ }
333
+ case "verify": {
334
+ const result = verifyInstallation({ root });
335
+ if (parsed.json) {
336
+ printResult(parsed, result);
337
+ }
338
+ else {
339
+ console.log(renderVerify(result));
340
+ }
341
+ return result.buildOk ? 0 : 1;
342
+ }
343
+ case "install": {
344
+ const plan = planInstallation({
345
+ root,
346
+ inspect,
347
+ workspaceId: parsed.workspaceId,
348
+ packageManager: parsed.packageManager,
349
+ artifacts
350
+ });
351
+ // Machine mode: preserve the exact legacy JSON contract.
352
+ if (parsed.json) {
353
+ if (!parsed.yes) {
354
+ printResult(parsed, plan);
355
+ console.error("Approval required before apply. Re-run with --yes to continue.");
356
+ maybePrintCommands(parsed.command, inspect.packageManager, parsed.workspaceId);
357
+ return plan.blockers.length === 0 ? 0 : 1;
358
+ }
359
+ if (!parsed.workspaceId) {
360
+ throw new Error("install requires --workspace <workspace-id> when --yes is used.");
361
+ }
362
+ const applyResult = applyInstallation({
363
+ root,
364
+ workspaceId: parsed.workspaceId,
365
+ plan,
366
+ allowDirty: parsed.allowDirty
367
+ });
368
+ const verifyResult = verifyInstallation({ root });
369
+ printResult(parsed, { inspect, plan, apply: applyResult, verify: verifyResult });
370
+ return verifyResult.buildOk ? 0 : 1;
371
+ }
372
+ // Human mode.
373
+ const issue = renderPlanIssue(plan);
374
+ if (issue) {
375
+ console.log(issue);
376
+ return plan.blockers.length === 0 ? 0 : 1;
377
+ }
378
+ if (parsed.yes) {
379
+ if (!parsed.workspaceId) {
380
+ throw new Error("install requires --workspace <workspace-id> when --yes is used.");
381
+ }
382
+ return applyAndRenderHuman({ root, inspect, plan, allowDirty: parsed.allowDirty });
383
+ }
384
+ // Preview, then either confirm interactively (TTY) or print how to apply.
385
+ console.log(renderPreview(plan));
386
+ const canApplyNow = Boolean(parsed.workspaceId);
387
+ if (process.stdin.isTTY && canApplyNow) {
388
+ const approved = await confirmApply();
389
+ if (!approved) {
390
+ console.log("\nNo changes made. Run it later with: npx infinite-tag install --yes\n");
391
+ return 0;
392
+ }
393
+ return applyAndRenderHuman({ root, inspect, plan, allowDirty: parsed.allowDirty });
394
+ }
395
+ const applyCommand = canApplyNow
396
+ ? "npx infinite-tag install --yes"
397
+ : "npx infinite-tag install --workspace <your-workspace-id> --yes";
398
+ console.log(`This was a preview — nothing changed. To apply: ${applyCommand}\n`);
399
+ return 0;
400
+ }
401
+ default:
402
+ throw new Error(`Unknown command: ${parsed.command}`);
403
+ }
404
+ }
405
+ catch (error) {
406
+ const message = error instanceof Error ? error.message : String(error);
407
+ console.error(message);
408
+ return 1;
409
+ }
410
+ }
411
+ const invokedAsScript = process.argv[1] && import.meta.url.endsWith(process.argv[1]);
412
+ if (invokedAsScript) {
413
+ void runCli().then((exitCode) => {
414
+ process.exitCode = exitCode;
415
+ });
416
+ }
@@ -0,0 +1,4 @@
1
+ import type { FrameworkAdapter, SupportedFramework } from "../types.js";
2
+ export declare const frameworkAdapters: FrameworkAdapter[];
3
+ export declare function getFrameworkAdapter(framework: string): FrameworkAdapter | undefined;
4
+ export declare function isSupportedFramework(framework: string): framework is SupportedFramework;