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.
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/src/apply.d.ts +14 -0
- package/dist/src/apply.js +84 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +416 -0
- package/dist/src/frameworks/index.d.ts +4 -0
- package/dist/src/frameworks/index.js +16 -0
- package/dist/src/frameworks/managed-files.d.ts +10 -0
- package/dist/src/frameworks/managed-files.js +104 -0
- package/dist/src/frameworks/next-app-router.d.ts +2 -0
- package/dist/src/frameworks/next-app-router.js +187 -0
- package/dist/src/frameworks/next-pages-router.d.ts +2 -0
- package/dist/src/frameworks/next-pages-router.js +169 -0
- package/dist/src/frameworks/shared.d.ts +17 -0
- package/dist/src/frameworks/shared.js +136 -0
- package/dist/src/frameworks/static-html.d.ts +2 -0
- package/dist/src/frameworks/static-html.js +137 -0
- package/dist/src/frameworks/vite-react.d.ts +2 -0
- package/dist/src/frameworks/vite-react.js +274 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.js +11 -0
- package/dist/src/inspect.d.ts +10 -0
- package/dist/src/inspect.js +150 -0
- package/dist/src/manifest.d.ts +10 -0
- package/dist/src/manifest.js +83 -0
- package/dist/src/package-manager.d.ts +6 -0
- package/dist/src/package-manager.js +110 -0
- package/dist/src/plan.d.ts +9 -0
- package/dist/src/plan.js +97 -0
- package/dist/src/providers/ga4.d.ts +2 -0
- package/dist/src/providers/ga4.js +73 -0
- package/dist/src/providers/index.d.ts +3 -0
- package/dist/src/providers/index.js +11 -0
- package/dist/src/providers/posthog.d.ts +2 -0
- package/dist/src/providers/posthog.js +77 -0
- package/dist/src/providers/validate.d.ts +31 -0
- package/dist/src/providers/validate.js +110 -0
- package/dist/src/providers/x.d.ts +2 -0
- package/dist/src/providers/x.js +76 -0
- package/dist/src/render.d.ts +25 -0
- package/dist/src/render.js +260 -0
- package/dist/src/types.d.ts +151 -0
- package/dist/src/types.js +8 -0
- package/dist/src/uninstall.d.ts +7 -0
- package/dist/src/uninstall.js +66 -0
- package/dist/src/verify.d.ts +5 -0
- package/dist/src/verify.js +50 -0
- package/dist/src/workspace-artifacts.d.ts +41 -0
- package/dist/src/workspace-artifacts.js +171 -0
- 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
|
+
}
|
package/dist/src/cli.js
ADDED
|
@@ -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;
|