@interfere/cli 0.1.5 → 1.0.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.
- package/README.md +55 -112
- package/dist/interfere.mjs +2 -0
- package/package.json +26 -35
- package/dist/bin.mjs +0 -42
package/README.md
CHANGED
|
@@ -1,143 +1,86 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Interfere CLI
|
|
2
2
|
|
|
3
|
-
Command-line
|
|
4
|
-
|
|
5
|
-
## Install
|
|
3
|
+
Command-line access to the Interfere API for developers and coding agents.
|
|
6
4
|
|
|
7
5
|
```sh
|
|
8
|
-
npm install -
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
pnpm add -D @interfere/cli
|
|
6
|
+
npm install -g @interfere/cli
|
|
7
|
+
interfere auth login --scheme oauth-production
|
|
8
|
+
export INTERFERE_WORKSPACE=your-workspace
|
|
9
|
+
interfere problems list
|
|
13
10
|
```
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
The CLI opens your browser for sign-in and organization selection. Authentication uses PKCE and stores credentials in your operating system's credential store. Linux static builds use a file restricted to your user. Expired access tokens refresh automatically.
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
## Discover commands
|
|
18
15
|
|
|
19
16
|
```sh
|
|
20
|
-
interfere
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### What it does
|
|
35
|
-
|
|
36
|
-
1. Auto-detects the CI runner and extracts commit SHA, branch, run ID, and run URL
|
|
37
|
-
2. Derives a release slug from the commit SHA (same algorithm the SDK uses at runtime)
|
|
38
|
-
3. Registers the release with the collector (`POST /v1/releases/`)
|
|
39
|
-
4. Discovers `.js.map` files in the target directory
|
|
40
|
-
5. Uploads source maps via presigned URLs
|
|
41
|
-
6. Marks the release as preflight-confirmed so the collector accepts spans for it
|
|
42
|
-
|
|
43
|
-
### Options
|
|
44
|
-
|
|
45
|
-
```text
|
|
46
|
-
interfere sourcemaps upload --source-maps <dir> [options]
|
|
47
|
-
|
|
48
|
-
Options:
|
|
49
|
-
--source-maps <dir> Directory containing .js.map files (required)
|
|
50
|
-
--release <slug> Override the release slug (default: derived from commit SHA)
|
|
51
|
-
--url <api-url> Interfere collector URL (default: $INTERFERE_API_URL)
|
|
52
|
-
--api-key <key> Interfere secret key (default: $INTERFERE_API_KEY)
|
|
53
|
-
--commit-sha <sha> Commit SHA (default: auto-detected from CI or git)
|
|
54
|
-
--commit-ref <ref> Branch or tag ref (default: auto-detected from CI or git)
|
|
55
|
-
--build-id <id> Build identifier (default: same as --commit-sha)
|
|
56
|
-
--runner <name> CI runner name override (default: auto-detected)
|
|
57
|
-
--run-id <id> CI run ID override (default: auto-detected)
|
|
58
|
-
--run-url <url> CI run URL override (default: auto-detected)
|
|
59
|
-
--skip-preflight Don't mark the release preflight-confirmed after upload
|
|
60
|
-
--dry-run Print what would be sent without making API calls
|
|
61
|
-
--json Output results as JSON
|
|
62
|
-
-h, --help Show help
|
|
63
|
-
--version Show version
|
|
17
|
+
interfere --help
|
|
18
|
+
interfere problems --help
|
|
19
|
+
interfere problems get --schema
|
|
20
|
+
interfere problems get --args.problem-id PROBLEM_ID
|
|
21
|
+
interfere audit list
|
|
22
|
+
interfere keys public --help
|
|
23
|
+
interfere keys private --help
|
|
24
|
+
interfere billing invoices list
|
|
25
|
+
interfere users invitations list
|
|
26
|
+
interfere notifications settings --help
|
|
27
|
+
interfere scim --help
|
|
28
|
+
interfere support --help
|
|
64
29
|
```
|
|
65
30
|
|
|
66
|
-
|
|
31
|
+
API commands come from the published API contract and support `--help`, `--schema`, and `--dry-run`. Use `--schema` to discover arguments before constructing a request. Dry runs show the request without sending it; the API still performs final validation when you execute it.
|
|
67
32
|
|
|
68
|
-
The
|
|
33
|
+
The command tree follows product workflows. Internal entitlements, subscriptions, presence, fingerprint rollups, and replay chunks are excluded from public discovery.
|
|
69
34
|
|
|
70
|
-
|
|
71
|
-
|---|---|---|---|---|---|
|
|
72
|
-
| GitHub Actions | `GITHUB_ACTIONS` | `GITHUB_SHA` | `GITHUB_HEAD_REF` / `GITHUB_REF_NAME` | `GITHUB_RUN_ID` | Constructed from `GITHUB_SERVER_URL` + `GITHUB_REPOSITORY` |
|
|
73
|
-
| CircleCI | `CIRCLECI` | `CIRCLE_SHA1` | `CIRCLE_BRANCH` | `CIRCLE_BUILD_NUM` | `CIRCLE_BUILD_URL` |
|
|
74
|
-
| GitLab CI | `GITLAB_CI` | `CI_COMMIT_SHA` | `CI_COMMIT_REF_NAME` | `CI_PIPELINE_ID` | `CI_PIPELINE_URL` |
|
|
75
|
-
| Buildkite | `BUILDKITE` | `BUILDKITE_COMMIT` | `BUILDKITE_BRANCH` | `BUILDKITE_BUILD_NUMBER` | `BUILDKITE_BUILD_URL` |
|
|
76
|
-
| Manual | (none) | `git rev-parse HEAD` | - | - | - |
|
|
35
|
+
## Inspect a problem
|
|
77
36
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
| Variable | Required | Purpose |
|
|
85
|
-
|---|---|---|
|
|
86
|
-
| `INTERFERE_API_KEY` | Yes | Authenticates with the collector. Format: `interfere_secret_us_*` or `interfere_secret_eu_*` |
|
|
87
|
-
| `INTERFERE_SOURCE_ID` | No | Override the commit SHA used to derive the release slug. Falls back to CI-detected SHA or `git rev-parse HEAD`. |
|
|
88
|
-
| `INTERFERE_API_URL` | No | Override the default collector URL |
|
|
37
|
+
```sh
|
|
38
|
+
interfere problems inspect PROBLEM_ID --workspace your-workspace
|
|
39
|
+
interfere problems inspect PROBLEM_ID --query '{problem: problem, findings: findings}' --format json
|
|
40
|
+
interfere problems inspect PROBLEM_ID --dry-run
|
|
41
|
+
```
|
|
89
42
|
|
|
90
|
-
|
|
43
|
+
`problems inspect` combines problem details, its timeline, and related findings. It exits with an error if either request fails. Its dry run lists the planned requests without contacting the API. This composite command supports `--help`; use `problems get --schema` and `findings list-by-problem --schema` for the underlying API schemas.
|
|
91
44
|
|
|
92
|
-
|
|
45
|
+
## Output for scripts and agents
|
|
93
46
|
|
|
94
|
-
|
|
95
|
-
- **At runtime (SDK):** reads from the same env vars or git
|
|
47
|
+
Piped output defaults to JSON. Use `--format json` to request JSON explicitly, or `--query` to select fields with JMESPath. Errors in machine output modes are JSON on stdout with a nonzero exit status. Check the exit status before using the output as a successful response.
|
|
96
48
|
|
|
97
|
-
|
|
49
|
+
```sh
|
|
50
|
+
interfere problems list --format json
|
|
51
|
+
interfere problems get --args.problem-id PROBLEM_ID --dry-run
|
|
52
|
+
```
|
|
98
53
|
|
|
99
|
-
##
|
|
54
|
+
## Update company context
|
|
100
55
|
|
|
101
|
-
|
|
56
|
+
```sh
|
|
57
|
+
interfere companies update-context --end-user-org-id COMPANY_ID \
|
|
58
|
+
--json '{"arr":{"amount":"120000.00","currency":"USD"},"tier":"Enterprise"}'
|
|
59
|
+
interfere companies get-context --end-user-org-id COMPANY_ID
|
|
60
|
+
interfere companies update-contexts \
|
|
61
|
+
--json '{"companies":[{"endUserOrgId":"COMPANY_ID","tier":"Enterprise"}]}'
|
|
62
|
+
```
|
|
102
63
|
|
|
103
|
-
|
|
104
|
-
- name: Build
|
|
105
|
-
run: npm run build
|
|
64
|
+
Use `--json` for partial updates. Omitted fields stay unchanged; `"arr":null` or `"tier":null` clears a field. ARR amounts are decimal strings in currency major units, with up to four decimal places. Zero revenue differs from unknown revenue. Tier names are case-sensitive and unique within the workspace; assigning a new name creates that tier. Bulk updates accept up to 100 distinct existing companies and either all succeed or all fail.
|
|
106
65
|
|
|
107
|
-
|
|
108
|
-
run: npx interfere sourcemaps upload ./dist
|
|
109
|
-
env:
|
|
110
|
-
INTERFERE_API_KEY: ${{ secrets.INTERFERE_API_KEY }}
|
|
111
|
-
# Runner, commit SHA, branch, run ID, and run URL are auto-detected
|
|
112
|
-
```
|
|
66
|
+
Company IDs come from `interfere companies list`. Reading context requires surface read permission; updating it requires surface write permission.
|
|
113
67
|
|
|
114
|
-
|
|
68
|
+
## Credentials and workspaces
|
|
115
69
|
|
|
116
|
-
|
|
117
|
-
- run:
|
|
118
|
-
name: Upload source maps
|
|
119
|
-
command: npx interfere sourcemaps upload ./dist
|
|
120
|
-
# CIRCLECI, CIRCLE_SHA1, etc. are auto-detected
|
|
121
|
-
```
|
|
70
|
+
`INTERFERE_ACCESS_TOKEN` supplies a user access token for noninteractive runs. `INTERFERE_SECRET_KEY` supplies a build secret for build API operations, and `INTERFERE_PUBLIC_KEY` supplies an ingestion public key. Each credential applies only to the API operations that accept it.
|
|
122
71
|
|
|
123
|
-
|
|
72
|
+
Set `INTERFERE_WORKSPACE` or pass `--workspace` for workspace operations. Named profiles can store workspace defaults and credentials. Use `interfere profiles --help` for profile commands. Explicit flags override profile values, and profile values override environment variables.
|
|
124
73
|
|
|
125
|
-
Preview
|
|
74
|
+
## Preview environment
|
|
126
75
|
|
|
127
76
|
```sh
|
|
128
|
-
|
|
77
|
+
export INTERFERE_ENVIRONMENT=preview
|
|
78
|
+
interfere auth login --scheme oauth-preview
|
|
79
|
+
interfere problems list --workspace your-preview-workspace
|
|
129
80
|
```
|
|
130
81
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
```dockerfile
|
|
134
|
-
ARG GIT_SHA
|
|
135
|
-
ENV INTERFERE_SOURCE_ID=$GIT_SHA
|
|
82
|
+
The default environment is `production`. Preview uses a separate OAuth client, API endpoint, and stored credential. Use separate profiles for workspaces in different environments.
|
|
136
83
|
|
|
137
|
-
|
|
138
|
-
CMD ["node", "dist/main.js"]
|
|
139
|
-
```
|
|
84
|
+
## Supported systems
|
|
140
85
|
|
|
141
|
-
|
|
142
|
-
docker build --build-arg GIT_SHA=$(git rev-parse HEAD) .
|
|
143
|
-
```
|
|
86
|
+
Native binaries are available for macOS on Apple Silicon and Intel, Linux on ARM64 and x64, and Windows on x64. npm selects the platform package through optional dependencies, so keep optional dependencies enabled. Installation does not run a download script.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{createRequire}from"node:module";import{spawnSync}from"node:child_process";const require=createRequire(import.meta.url),platform=`${process.platform}-${process.arch}`;new Set([`darwin-arm64`,`darwin-x64`,`linux-arm64`,`linux-x64`,`win32-x64`]).has(platform)||(process.stderr.write(`Interfere CLI does not support ${platform}.\n`),process.exit(1));let binary;try{binary=require.resolve(`@interfere/cli-${platform}/bin/interfere${process.platform===`win32`?`.exe`:``}`)}catch{process.stderr.write(`Missing @interfere/cli-${platform}. Reinstall @interfere/cli with optional dependencies enabled.\n`),process.exit(1)}const result=spawnSync(binary,process.argv.slice(2),{stdio:`inherit`});result.error&&(process.stderr.write(`Unable to start Interfere CLI: ${result.error.message}\n`),process.exit(1)),result.signal?process.kill(process.pid,result.signal):process.exit(result.status??1);export{};
|
package/package.json
CHANGED
|
@@ -1,51 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interfere/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "Command-line tool for Interfere. Uploads source maps and confirms releases as part of your CI deploy.",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"observability",
|
|
8
|
-
"cli",
|
|
9
|
-
"source-maps",
|
|
10
|
-
"release-management",
|
|
11
|
-
"interfere"
|
|
12
|
-
],
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Command-line access to the Interfere API for developers and coding agents.",
|
|
13
5
|
"homepage": "https://interfere.com",
|
|
14
|
-
"
|
|
15
|
-
"url": "mailto:support@interfere.com"
|
|
16
|
-
},
|
|
6
|
+
"license": "MIT",
|
|
17
7
|
"author": "Interfere <support@interfere.com> (https://interfere.com)",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"url": "git+https://interfere.ghe.com/interfere/interfere.git",
|
|
21
|
-
"directory": "src/packages/public/cli"
|
|
8
|
+
"bin": {
|
|
9
|
+
"interfere": "dist/interfere.mjs"
|
|
22
10
|
},
|
|
23
11
|
"files": [
|
|
24
|
-
"dist"
|
|
12
|
+
"dist/interfere.mjs",
|
|
13
|
+
"README.md"
|
|
25
14
|
],
|
|
26
15
|
"type": "module",
|
|
27
|
-
"bin": {
|
|
28
|
-
"interfere": "./dist/bin.mjs"
|
|
29
|
-
},
|
|
30
16
|
"publishConfig": {
|
|
31
17
|
"access": "public"
|
|
32
18
|
},
|
|
33
19
|
"scripts": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"typecheck": "tsc --noEmit"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"@interfere/constants": "^9.0.6",
|
|
40
|
-
"@interfere/types": "^10.1.2"
|
|
20
|
+
"typegen": "fern generate --local --force && yq -i '.package.version = strenv(npm_package_version)' Cargo.toml && yq -i '.package.version = strenv(npm_package_version)' generated/Cargo.toml && cargo update --workspace && cargo update --manifest-path generated/Cargo.toml --workspace",
|
|
21
|
+
"build": "tsdown && cargo build --locked --release",
|
|
22
|
+
"typecheck": "tsc --noEmit && cargo fmt --check && cargo clippy --locked --bin interfere --tests -- -D warnings",
|
|
23
|
+
"test": "cargo test --locked",
|
|
24
|
+
"test:wire": "cargo test --locked --manifest-path generated/Cargo.toml --target-dir target --test wire_test"
|
|
41
25
|
},
|
|
42
26
|
"devDependencies": {
|
|
43
|
-
"@
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
27
|
+
"@types/node": "^26.4.1",
|
|
28
|
+
"fern-api": "https://github.com/skve/fern/releases/download/pr-17949-ffdd423/fern-api-5.143.1-pr.17949.ffdd423.tgz",
|
|
29
|
+
"tsdown": "^0.23.0",
|
|
30
|
+
"typescript": "^7.0.2"
|
|
31
|
+
},
|
|
32
|
+
"optionalDependencies": {
|
|
33
|
+
"@interfere/cli-darwin-arm64": "1.0.0",
|
|
34
|
+
"@interfere/cli-darwin-x64": "1.0.0",
|
|
35
|
+
"@interfere/cli-linux-arm64": "1.0.0",
|
|
36
|
+
"@interfere/cli-linux-x64": "1.0.0",
|
|
37
|
+
"@interfere/cli-win32-x64": "1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
50
41
|
}
|
|
51
42
|
}
|
package/dist/bin.mjs
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import{API_URL}from"@interfere/constants/api";import path from"node:path";import{readFileSync}from"node:fs";import{readdir}from"node:fs/promises";import{parseArgs}from"node:util";import{createReleaseResponse}from"@interfere/types/releases/definition";import{readFirstEnvValue}from"@interfere/types/sdk/env";import{execFileSync}from"node:child_process";import{releaseSourceIdEnvKeys}from"@interfere/types/integrations";import{deriveReleaseSlug}from"@interfere/types/releases/slug";import{createHash,randomUUID}from"node:crypto";async function discoverSourceMaps(root){let found=[];return await walk(root,root,found),found}async function walk(root,dir,out){let entries=await readdir(dir,{withFileTypes:!0});for(let entry of entries){let abs=path.join(dir,entry.name);if(entry.isDirectory()){if(entry.name===`node_modules`||entry.name.startsWith(`.`))continue;await walk(root,abs,out);continue}entry.name.endsWith(`.js.map`)&&out.push({absPath:abs,relPath:path.relative(root,abs),content:readFileSync(abs,`utf-8`)})}}var name=`@interfere/cli`,version=`0.1.5`;const VERSION=version,PRODUCER_VERSION=`${name}@${version}`,HELP=`
|
|
3
|
-
interfere ${VERSION}
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
interfere sourcemaps upload --source-maps <dir> [options]
|
|
7
|
-
interfere --help
|
|
8
|
-
interfere --version
|
|
9
|
-
|
|
10
|
-
Options:
|
|
11
|
-
--source-maps <dir> Directory containing .js.map files (required)
|
|
12
|
-
--release <slug> Release slug (default: derived from commit SHA)
|
|
13
|
-
--url <api-url> Interfere collector URL (default: $INTERFERE_API_URL)
|
|
14
|
-
--api-key <key> Interfere secret key (default: $INTERFERE_API_KEY)
|
|
15
|
-
--auth-token <key> Deprecated alias for --api-key
|
|
16
|
-
--commit-sha <sha> Commit SHA (default: auto-detected from CI or git)
|
|
17
|
-
--commit-ref <ref> Branch or tag ref (default: auto-detected from CI or git)
|
|
18
|
-
--build-id <id> Build identifier (default: same as --commit-sha)
|
|
19
|
-
--runner <name> CI runner name override (default: auto-detected)
|
|
20
|
-
--run-id <id> CI run ID override (default: auto-detected)
|
|
21
|
-
--run-url <url> CI run URL override (default: auto-detected)
|
|
22
|
-
--environment <env> Deployment environment (e.g. production, preview)
|
|
23
|
-
--deployment-url <u> URL of the deployed application (optional)
|
|
24
|
-
--skip-preflight Don't mark the release preflight-confirmed after publishing metadata
|
|
25
|
-
--dry-run Print what would be sent without making API calls
|
|
26
|
-
--json Output results as JSON
|
|
27
|
-
-h, --help Show this help
|
|
28
|
-
--version Show version
|
|
29
|
-
|
|
30
|
-
Environment:
|
|
31
|
-
INTERFERE_API_KEY Interfere secret key in the interfere_secret_{us|eu}_* format
|
|
32
|
-
INTERFERE_API_URL Override the default collector URL
|
|
33
|
-
INTERFERE_SOURCE_ID Override the commit SHA used to derive the slug.
|
|
34
|
-
Falls back to VERCEL_GIT_COMMIT_SHA, GITHUB_SHA,
|
|
35
|
-
or \`git rev-parse HEAD\`.
|
|
36
|
-
|
|
37
|
-
CI Auto-Detection:
|
|
38
|
-
GitHub Actions GITHUB_ACTIONS, GITHUB_SHA, GITHUB_REF_NAME, GITHUB_RUN_ID
|
|
39
|
-
CircleCI CIRCLECI, CIRCLE_SHA1, CIRCLE_BRANCH, CIRCLE_BUILD_NUM
|
|
40
|
-
GitLab CI GITLAB_CI, CI_COMMIT_SHA, CI_COMMIT_REF_NAME, CI_PIPELINE_ID
|
|
41
|
-
Buildkite BUILDKITE, BUILDKITE_COMMIT, BUILDKITE_BRANCH, BUILDKITE_BUILD_NUMBER
|
|
42
|
-
`,OPTIONS={release:{type:`string`},url:{type:`string`},"api-key":{type:`string`},"auth-token":{type:`string`},"commit-sha":{type:`string`},"commit-ref":{type:`string`},"build-id":{type:`string`},runner:{type:`string`},"run-id":{type:`string`},"run-url":{type:`string`},"source-maps":{type:`string`},environment:{type:`string`},"deployment-url":{type:`string`},"skip-preflight":{type:`boolean`},"dry-run":{type:`boolean`},json:{type:`boolean`},help:{type:`boolean`,short:`h`},version:{type:`boolean`}};function parseFlags(argv){let values,positionals;try{({values,positionals}=parseArgs({args:[...argv],options:OPTIONS,allowPositionals:!0}))}catch(error){process.stderr.write(`interfere: ${error instanceof Error?error.message:String(error)}\n`),process.exit(1)}values.help&&(process.stdout.write(`${HELP.trim()}\n`),process.exit(0)),values.version&&(process.stdout.write(`${VERSION}\n`),process.exit(0));let str=key=>typeof values[key]==`string`?values[key]:null;return{apiKey:str(`api-key`)??str(`auth-token`),buildId:str(`build-id`),commitRef:str(`commit-ref`),commitSha:str(`commit-sha`),deploymentUrl:str(`deployment-url`),dryRun:values[`dry-run`]===!0,environment:str(`environment`),json:values.json===!0,positional:positionals,release:str(`release`),runId:str(`run-id`),runner:str(`runner`),runUrl:str(`run-url`),skipPreflight:values[`skip-preflight`]===!0,sourceMaps:str(`source-maps`),url:str(`url`)}}async function confirmPreflight({apiKey,apiUrl,releaseSlug}){let url=`${apiUrl}/v1/releases/${encodeURIComponent(releaseSlug)}/preflight`,response=await fetch(url,{method:`POST`,headers:{"content-type":`application/json`,"user-agent":PRODUCER_VERSION,Authorization:`Bearer ${apiKey}`}});if(!response.ok){let detail=await response.text().catch(()=>``);throw Error(`POST ${url} → ${response.status} ${detail}`)}}function git(args){try{let out=execFileSync(`git`,[...args],{encoding:`utf-8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim();return out.length>0?out:null}catch{return null}}function gitHead(){return git([`rev-parse`,`HEAD`])}async function ensureRelease({apiKey,apiUrl,buildId,commitSha,deploymentUrl,dryRun,environment,releaseSlug,runner}){let body={source:{provider:`cli`,commitSha,commitRef:runner.commitRef??resolveBranchFallback(),commitMessage:resolveCommitMessage(),commitUrl:null,runnerName:runner.runner===`manual`?null:runner.runner,runId:runner.runId,runUrl:runner.runUrl,environment:environment??null,deploymentUrl:deploymentUrl??null},destination:{provider:`cli`,destinationReleaseId:releaseSlug,environment:environment??null,deploymentUrl:deploymentUrl??null},buildId,slug:releaseSlug,producerVersion:PRODUCER_VERSION};if(dryRun)return{body,response:null};let url=`${apiUrl}/v1/releases/`,res=await fetch(url,{method:`POST`,headers:{"content-type":`application/json`,accept:`application/json`,"user-agent":PRODUCER_VERSION,Authorization:`Bearer ${apiKey}`},body:JSON.stringify(body)});if(!res.ok){let detail=await res.text().catch(()=>``);throw Error(`POST ${url} → ${res.status} ${detail}`)}let parsed=createReleaseResponse.safeParse(await res.json());if(!parsed.success)throw Error(`POST ${url} → unexpected response shape: ${parsed.error.message}`);return{body,response:parsed.data}}function resolveCommitMessage(){return readFirstEnvValue(process.env,[`VERCEL_GIT_COMMIT_MESSAGE`])??git([`log`,`-1`,`--format=%s`])??``}function resolveBranchFallback(){return readFirstEnvValue(process.env,[`VERCEL_GIT_COMMIT_REF`])??git([`rev-parse`,`--abbrev-ref`,`HEAD`])??`unknown`}const detectors=[detectGitHubActions,detectSimpleRunner],SIMPLE_RUNNERS=[{signal:`CIRCLECI`,runner:`circleci`,runId:`CIRCLE_BUILD_NUM`,runUrl:`CIRCLE_BUILD_URL`,commitSha:`CIRCLE_SHA1`,commitRef:`CIRCLE_BRANCH`},{signal:`GITLAB_CI`,runner:`gitlab-ci`,runId:`CI_PIPELINE_ID`,runUrl:`CI_PIPELINE_URL`,commitSha:`CI_COMMIT_SHA`,commitRef:`CI_COMMIT_REF_NAME`},{signal:`BUILDKITE`,runner:`buildkite`,runId:`BUILDKITE_BUILD_NUMBER`,runUrl:`BUILDKITE_BUILD_URL`,commitSha:`BUILDKITE_COMMIT`,commitRef:`BUILDKITE_BRANCH`}];function detectRunner({env,gitFallback,overrides}){let result;for(let detect of detectors){let detected=detect(env);if(detected){result=detected;break}}let base=result??{runner:`manual`,runId:null,runUrl:null,commitSha:gitFallback(),commitRef:null,environment:null};return overrides?{...base,...overrides.runner?{runner:overrides.runner}:{},...overrides.runId?{runId:overrides.runId}:{},...overrides.runUrl?{runUrl:overrides.runUrl}:{},...overrides.commitSha?{commitSha:overrides.commitSha}:{},...overrides.commitRef?{commitRef:overrides.commitRef}:{}}:base}function detectGitHubActions(env){if(!env.GITHUB_ACTIONS)return null;let serverUrl=env.GITHUB_SERVER_URL??`https://github.com`,repo=env.GITHUB_REPOSITORY,runId=env.GITHUB_RUN_ID??null;return{runner:`github-actions`,runId,runUrl:repo&&runId?`${serverUrl}/${repo}/actions/runs/${runId}`:null,commitSha:env.GITHUB_SHA??null,commitRef:env.GITHUB_HEAD_REF||env.GITHUB_REF_NAME||null,environment:null}}function detectSimpleRunner(env){for(let spec of SIMPLE_RUNNERS)if(env[spec.signal])return{runner:spec.runner,runId:env[spec.runId]??null,runUrl:env[spec.runUrl]??null,commitSha:env[spec.commitSha]??null,commitRef:env[spec.commitRef]??null,environment:null};return null}function resolveReleaseSlug(commitSha){return{commitSha,slug:commitSha?deriveReleaseSlug(commitSha):null}}function resolveApiKey(flags){let apiKey=flags.apiKey??process.env.INTERFERE_API_KEY;if(!apiKey){if(flags.dryRun)return``;die$1(`Missing secret key. Set INTERFERE_API_KEY or pass --api-key.`)}return apiKey}function resolveRunner(flags){return detectRunner({env:process.env,gitFallback:gitHead,overrides:{runner:flags.runner,runId:flags.runId,runUrl:flags.runUrl,commitSha:flags.commitSha,commitRef:flags.commitRef}})}function resolveIdentity(flags,runner){let commitSha=flags.commitSha?runner.commitSha:readFirstEnvValue(process.env,releaseSourceIdEnvKeys)??runner.commitSha;commitSha||die$1(`Could not determine commit SHA. Pass --commit-sha, set INTERFERE_SOURCE_ID, or run inside a git repo.`);let releaseSlug=flags.release;if(!releaseSlug){let resolved=resolveReleaseSlug(commitSha);resolved.slug||die$1(`Could not derive a release slug from commit SHA. Pass --release explicitly.`),releaseSlug=resolved.slug}return{releaseSlug,commitSha,buildId:flags.buildId??commitSha}}function die$1(message){process.stderr.write(`interfere: ${message}\n`),process.exit(1)}const MAP_SUFFIX_RE=/\.map$/u;function prepareEntries(maps){return maps.map(map=>{let debugId=randomUUID(),content=injectDebugId(map.content,debugId);return{relPath:map.relPath,chunkUrl:map.relPath.replace(MAP_SUFFIX_RE,``),debugId,content,contentBytes:Buffer.byteLength(content,`utf-8`),hash:createHash(`sha256`).update(content).digest(`hex`)}})}function injectDebugId(rawMap,debugId){let parsed=JSON.parse(rawMap);return JSON.stringify({...parsed,debugId})}async function uploadSourceMaps({apiKey,apiUrl,maps,releaseSlug}){let entries=prepareEntries(maps),slug=encodeURIComponent(releaseSlug),signReq={files:entries.map(entry=>({path:entry.relPath,sizeBytes:entry.contentBytes,...richness(entry.content)}))},signRes=await postJson({apiKey,url:`${apiUrl}/v1/releases/${slug}/source-maps/sign`,body:signReq}),byPath=new Map(entries.map(entry=>[entry.relPath,entry])),totalBytes=0;await mapWithConcurrency(signRes.uploads,32,async upload=>{let entry=byPath.get(upload.path);if(!entry)throw Error(`Sign response referenced unknown path "${upload.path}"`);totalBytes+=entry.contentBytes,await putToR2(upload.presignedUrl,entry.content)});let completeReq={files:entries.map(entry=>({path:entry.relPath,hash:entry.hash,debugId:entry.debugId,chunkUrl:entry.chunkUrl})),sourceFileCount:entries.length,bundler:`tsc`};return{fileCount:(await postJson({apiKey,url:`${apiUrl}/v1/releases/${slug}/source-maps/complete`,body:completeReq})).fileCount,totalBytes}}function richness(content){let parsed=JSON.parse(content);return{hasSourcesContent:Array.isArray(parsed.sourcesContent)&&parsed.sourcesContent.length>0,hasNames:Array.isArray(parsed.names)&&parsed.names.length>0,hasFile:typeof parsed.file==`string`&&parsed.file.length>0,mappingsPresent:typeof parsed.mappings==`string`&&parsed.mappings.length>0}}async function postJson({apiKey,url,body}){let response=await fetch(url,{method:`POST`,headers:{"content-type":`application/json`,accept:`application/json`,"user-agent":PRODUCER_VERSION,Authorization:`Bearer ${apiKey}`},body:JSON.stringify(body)});if(!response.ok){let detail=await response.text().catch(()=>``);throw Error(`POST ${url} → ${response.status} ${detail}`)}return await response.json()}async function putToR2(presignedUrl,content){let response=await fetch(presignedUrl,{method:`PUT`,headers:{"content-type":`application/json`},body:content});if(!response.ok){let detail=await response.text().catch(()=>``);throw Error(`PUT R2 → ${response.status} ${detail}`)}}async function mapWithConcurrency(items,concurrency,fn){let cursor=0;async function worker(){for(;cursor<items.length;){let i=cursor;cursor+=1;let item=items[i];item!==void 0&&await fn(item)}}await Promise.all(Array.from({length:Math.min(concurrency,items.length)},()=>worker()))}function log(flags,message){flags.json||console.log(message)}function formatBytes(n){return n<1024?`${n} B`:n<1048576?`${(n/1024).toFixed(1)} KB`:`${(n/1048576).toFixed(1)} MB`}function die(message){process.stderr.write(`interfere: ${message}\n`),process.exit(1)}async function discoverMaps(absDir){try{return await discoverSourceMaps(absDir)}catch(error){die(`Could not read source maps directory ${absDir}: ${error instanceof Error?error.message:String(error)}`)}}function printSummary(flags,result){log(flags,`Done. Release ${result.slug} — ${result.files} file(s), ${formatBytes(result.bytes)}.`),result.url&&log(flags,result.url),flags.json&&process.stdout.write(`${JSON.stringify(result,null,2)}\n`)}async function sourcemapsUpload(dir,flags){let apiKey=resolveApiKey(flags),apiUrl=flags.url??process.env.INTERFERE_API_URL??API_URL,runner=resolveRunner(flags),{releaseSlug,commitSha,buildId}=resolveIdentity(flags,runner);flags.release||log(flags,`Derived release ${releaseSlug} from commit ${commitSha}`);let absDir=path.resolve(dir),maps=await discoverMaps(absDir),{body:releaseBody,response:releaseRes}=await ensureRelease({apiKey,apiUrl,buildId,commitSha,deploymentUrl:flags.deploymentUrl,dryRun:flags.dryRun,environment:flags.environment,releaseSlug,runner}),dashboardUrl=releaseRes?`https://interfere.com/~/${releaseRes.org.slug}/surfaces/${releaseRes.surface.slug}`:null;if(flags.dryRun){if(flags.json){let output={release:releaseBody,runner,files:maps.map(map=>map.relPath),dryRun:!0};process.stdout.write(`${JSON.stringify(output,null,2)}\n`)}else log(flags,`[dry-run] Release payload:`),log(flags,JSON.stringify(releaseBody,null,2)),log(flags,`[dry-run] Runner: ${runner.runner}`),log(flags,`[dry-run] Would upload ${maps.length} source map(s) from ${absDir}`);return}if(maps.length===0){log(flags,`No .js.map files found under ${absDir}`),flags.skipPreflight||await confirmPreflight({apiKey,apiUrl,releaseSlug}),printSummary(flags,{slug:releaseSlug,buildId,commitSha,runner:runner.runner,files:0,bytes:0,url:dashboardUrl});return}log(flags,`Uploading ${maps.length} source map(s) to ${releaseSlug}…`);let summary=await uploadSourceMaps({apiKey,apiUrl,maps,releaseSlug});flags.skipPreflight||await confirmPreflight({apiKey,apiUrl,releaseSlug}),printSummary(flags,{slug:releaseSlug,buildId,commitSha,runner:runner.runner,files:summary.fileCount,bytes:summary.totalBytes,url:dashboardUrl})}async function runCli(argv=process.argv.slice(2)){if(argv.length===0){parseFlags([`--help`]);return}let flags=parseFlags(argv),[command,sub,...rest]=flags.positional;if(command===`sourcemaps`&&sub===`upload`){let dir=flags.sourceMaps??rest[0];dir||die(`Missing source maps directory. Pass --source-maps <dir> or: interfere sourcemaps upload <dir>`),await sourcemapsUpload(dir,flags);return}die(`Unknown command: ${flags.positional.join(` `)||`(none)`}. Try --help.`)}await runCli();export{};
|