google-apis-cli 0.1.0 → 0.3.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 +42 -26
- package/dist/bin/gapi.js +11 -135
- package/dist/src/apis/ads.js +59 -49
- package/dist/src/apis/bid-manager.js +4 -0
- package/dist/src/apis/bigquery-dts.js +4 -0
- package/dist/src/apis/business-account-management.js +5 -0
- package/dist/src/apis/business-information.js +5 -0
- package/dist/src/apis/business-lodging.js +5 -0
- package/dist/src/apis/business-notifications.js +5 -0
- package/dist/src/apis/business-performance.js +5 -0
- package/dist/src/apis/business-place-actions.js +5 -0
- package/dist/src/apis/business-v4.js +52 -0
- package/dist/src/apis/business-verifications.js +5 -0
- package/dist/src/apis/cm360.js +4 -0
- package/dist/src/apis/crux.js +8 -0
- package/dist/src/apis/data-manager.js +4 -0
- package/dist/src/apis/dv360.js +4 -0
- package/dist/src/apis/ga4-admin-alpha.js +4 -0
- package/dist/src/apis/ga4-admin.js +1 -288
- package/dist/src/apis/ga4-data-alpha.js +5 -0
- package/dist/src/apis/ga4-data.js +9 -69
- package/dist/src/apis/gsc.js +6 -58
- package/dist/src/apis/gtm.js +1 -189
- package/dist/src/apis/indexing.js +37 -15
- package/dist/src/apis/merchant-accounts.js +4 -0
- package/dist/src/apis/merchant-conversions.js +4 -0
- package/dist/src/apis/merchant-datasources.js +4 -0
- package/dist/src/apis/merchant-inventories.js +4 -0
- package/dist/src/apis/merchant-issueresolution.js +4 -0
- package/dist/src/apis/merchant-lfp.js +4 -0
- package/dist/src/apis/merchant-loyaltycustomers.js +4 -0
- package/dist/src/apis/merchant-notifications.js +4 -0
- package/dist/src/apis/merchant-ordertracking.js +4 -0
- package/dist/src/apis/merchant-products.js +4 -0
- package/dist/src/apis/merchant-promotions.js +4 -0
- package/dist/src/apis/merchant-quota.js +4 -0
- package/dist/src/apis/merchant-reports.js +5 -0
- package/dist/src/apis/merchant-reviews.js +4 -0
- package/dist/src/apis/pagespeed.js +8 -0
- package/dist/src/apis/places.js +19 -0
- package/dist/src/apis/postmaster.js +4 -0
- package/dist/src/apis/sa360-legacy.js +4 -0
- package/dist/src/apis/sa360.js +13 -0
- package/dist/src/apis/sheets.js +4 -0
- package/dist/src/apis/site-verification.js +4 -0
- package/dist/src/apis/web-risk.js +18 -0
- package/dist/src/apis/youtube-analytics.js +4 -0
- package/dist/src/apis/youtube-reporting.js +4 -0
- package/dist/src/apis/youtube.js +4 -0
- package/dist/src/catalog.js +218 -0
- package/dist/src/credentials.js +342 -0
- package/dist/src/errors.js +16 -0
- package/dist/src/json.js +11 -0
- package/dist/src/plan.js +348 -0
- package/dist/src/render.js +248 -0
- package/dist/src/responses.js +37 -0
- package/dist/src/run.js +374 -0
- package/dist/src/search.js +53 -0
- package/dist/src/transport/batch.js +75 -0
- package/dist/src/transport/download.js +21 -0
- package/dist/src/transport/retry.js +21 -0
- package/dist/src/transport/upload.js +106 -0
- package/dist/src/transport.js +125 -0
- package/dist/src/types.js +1 -13
- package/package.json +23 -8
- package/dist/src/apis/ga4-admin-v1alpha.js +0 -280
- package/dist/src/auth.js +0 -172
- package/dist/src/config.js +0 -89
- package/dist/src/manifest.js +0 -69
- package/dist/src/rest.js +0 -90
package/README.md
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
# google-apis-cli (`gapi`)
|
|
2
2
|
|
|
3
|
-
`gapi` is one CLI for Google
|
|
3
|
+
`gapi` is one CLI for 47 Google API entries, built from Google's discovery documents at runtime:
|
|
4
4
|
|
|
5
|
-
-
|
|
5
|
+
- Google Ads v26, GA4 Admin (v1beta, v1alpha), GA4 Data (v1beta, v1alpha), Search Console, Tag Manager, Indexing
|
|
6
|
+
- Merchant API (13 v1 sub-APIs + Reviews v1alpha), Business Profile (7 v1 APIs + v4)
|
|
7
|
+
- Search Ads 360 (Reporting v0, legacy v2), Campaign Manager 360 v5, Display & Video 360 v4, Bid Manager v2
|
|
8
|
+
- YouTube Data v3, YouTube Analytics v2, YouTube Reporting v1
|
|
9
|
+
- PageSpeed Insights v5, CrUX v1 (API key)
|
|
10
|
+
- BigQuery Data Transfer v1, Data Manager v1, Gmail Postmaster Tools v2, Site Verification v1, Sheets v4, Places (New) v1, Web Risk v1
|
|
11
|
+
|
|
12
|
+
`gapi --help` lists the entry names; [skills/gapi/reference/index.md](skills/gapi/reference/index.md) gives title, version, method count and auth per entry.
|
|
13
|
+
|
|
14
|
+
- One login. One refresh token. Every scope the entries use (computed from the documents). Read and write.
|
|
6
15
|
- One runtime dependency: `google-auth-library` (Google). Nothing third-party.
|
|
7
16
|
- All network calls use native `fetch`.
|
|
8
|
-
- TypeScript 7 (strict), Node
|
|
17
|
+
- TypeScript 7 (strict), Node 26. Node runs `.ts` directly (type-stripping) — no build step in dev. Biome for lint + format. pnpm 12 (pinned via `packageManager`) for package management.
|
|
9
18
|
- It replaces the 4 fragmented Google MCPs in vmCODE.
|
|
10
19
|
|
|
11
|
-
Command shape: `gapi <api> <resource> <
|
|
20
|
+
Command shape: `gapi <api> <resource>… <method> [--params '<json>'] [--body '<json>'] [--upload <file> [--upload-content-type <type>] [--upload-protocol media|multipart|resumable]] [--output <file>] [--page-all [--page-limit <n>]] [--cache <age>] [--dry-run]`. `gapi <api> [<resource>…]` lists a level (the api level also lists helpers); `… <method> --help` shows method help. `gapi <api> +<helper> … [--dry-run] [--cache <age>] [--project <id>] [--api-key <key>] [<entry header flag>]` runs a hand command, e.g. `gapi ads +gaql "<query>" --customer <id> [--stream]`, `gapi indexing +publishBatch <file>`. `gapi search <words…>` lists at most 20 methods across all API entries that match the words (command, marks, HTTP method, first sentence of the description). `gapi schema <api> <resource>[.<sub>…].<method> | <SchemaName> [--resolve-refs]` prints a method shape or a schema as JSON; `--find <word>` prints instead only the field paths of the expanded schema (a method: `request.…` and `response.…`) whose path or printed description (first sentence, max 100 chars) contains the word. `gapi discovery refresh [<api>…]` refetches discovery documents now; `gapi discovery path` prints the cache dir. `gapi search --help`, `gapi schema --help`, `gapi discovery --help` and `gapi auth [<sub>] --help` print that command's usage and run nothing.
|
|
12
21
|
|
|
13
22
|
## Install
|
|
14
23
|
|
|
@@ -16,57 +25,64 @@ Command shape: `gapi <api> <resource> <verb> [--json|--raw]`
|
|
|
16
25
|
npm i -g google-apis-cli # installs the `gapi` bin
|
|
17
26
|
```
|
|
18
27
|
|
|
19
|
-
Needs **Node ≥
|
|
28
|
+
Needs **Node ≥ 26**. The package ships `dist/` (plain `tsc` output, built at publish, no bundle); in the repo, Node runs the `.ts` sources directly.
|
|
20
29
|
|
|
21
30
|
## Start here
|
|
22
31
|
|
|
23
32
|
```
|
|
24
|
-
gapi auth setup # store your own OAuth Desktop client (id + secret)
|
|
25
|
-
gapi auth login # one consent
|
|
26
|
-
gapi auth status # account, refresh token, granted scopes
|
|
33
|
+
gapi auth setup # store your own OAuth Desktop client (id + secret), optional ids and API key
|
|
34
|
+
gapi auth login # one consent for every scope the API entries use
|
|
35
|
+
gapi auth status # account, refresh token, API key, granted and missing scopes
|
|
27
36
|
```
|
|
28
37
|
|
|
29
|
-
First run, or a console step fails? **[docs/SETUP.md](docs/SETUP.md)** walks the
|
|
30
|
-
the traps in each. `gapi` is multi-tenant: every operator brings their own Cloud project.
|
|
38
|
+
First run, or a console step fails? **[docs/SETUP.md](docs/SETUP.md)** walks the steps (services,
|
|
39
|
+
API key, Business Profile access, consent, Merchant registration) and the traps in each. `gapi` is multi-tenant: every operator brings their own Cloud project.
|
|
31
40
|
|
|
32
41
|
## OAuth App Verification
|
|
33
42
|
|
|
34
|
-
`gapi` asks for sensitive scopes (`analytics.manage.users`, `tagmanager.manage.*`, `adwords`, `webmasters`).
|
|
43
|
+
`gapi` asks for sensitive scopes (e.g. `analytics.manage.users`, `tagmanager.manage.*`, `adwords`, `webmasters`). `gapi auth status` shows the granted and the missing scopes. Never requested: `drive`/`drive.*` (restricted) and `display-video-user-management` (Google rejects it for this app).
|
|
35
44
|
|
|
36
|
-
- **Testing mode** (default) works for all commands, but the refresh token expires
|
|
37
|
-
- **
|
|
45
|
+
- **Testing mode** (default) works for all commands, but the refresh token expires after 7 days — re-run `gapi auth login` when it does. [Google](https://developers.google.com/identity/protocols/oauth2#expiration): "a publishing status of "Testing" is issued a refresh token expiring in 7 days".
|
|
46
|
+
- **In production** (`Publish app`) removes that expiry. It needs home page, privacy policy and terms of service links ([Google](https://support.google.com/cloud/answer/15549049): "These links are required for all external production apps."). Verification is not a precondition: an unverified production app shows the "unverified app" warning on consent and has a cap of 100 new users ([Manage App Audience](https://support.google.com/cloud/answer/15549945)).
|
|
47
|
+
- **OAuth App Verification** removes the warning and the cap. Personal use does not need it ([Google](https://support.google.com/cloud/answer/13464323): "If the app is for your personal use (fewer than 100 users), you and your limited number of users can continue using the app without going through verification"). Review timeline is not guaranteed.
|
|
38
48
|
|
|
39
|
-
Testing mode is enough to use the CLI;
|
|
49
|
+
Testing mode is enough to use the CLI; production is the no-expiry target. Step 4 of [docs/SETUP.md](docs/SETUP.md) walks the switch.
|
|
40
50
|
|
|
41
51
|
## Flow
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
A call moves through these parts in order.
|
|
44
54
|
|
|
45
55
|
| Part | File | Function |
|
|
46
56
|
|---|---|---|
|
|
47
|
-
| **
|
|
48
|
-
| **
|
|
49
|
-
| **
|
|
57
|
+
| **run** | `src/run.ts` | `run(argv, io)`: strict argv parse, routing (`--help`, `auth`, `search`, `schema`, `discovery`, `<api> …`), `GapiError` → exit code. `bin/gapi.ts` passes process io. |
|
|
58
|
+
| **Catalog** | `src/catalog.ts` | API entries (`src/apis/*.ts`, glob, no registry) → `ApiModel`. Discovery entries: document cached 24 h. |
|
|
59
|
+
| **Planner** | `src/plan.ts` | `plan(model, words \| methodId, input, settings)` → `CallPlan`. Pure. Every `--params`/`--body` rule; validation fails exit 3 before network. |
|
|
60
|
+
| **Transport** | `src/transport.ts` + `src/transport/{upload,download,batch}.ts` | `send(plan, { fetch, authorize, out, sleep })`. `authorize` adds the credential per request; GET/HEAD/PUT/DELETE and `reads` POSTs retry 408/429/5xx and network errors (max 3, backoff with jitter); non-2xx or network failure → exit 1; 403 `ACCESS_TOKEN_SCOPE_INSUFFICIENT` (also in an array body or a batch part) → exit 2. `--page-all` follows `nextPageToken`, sending `pageToken` in the query or in the request body (methods whose request schema has `pageToken`): one JSON line per page, max `--page-limit` (default 10). `--upload` streams a file to `rootUrl` + `upload/` (media, multipart, or resumable in 8 MiB chunks, resumed from the session's `Range` after a 5xx or network error); `--output` writes the `alt=media` body to a file, from `rootUrl` + `download/` when the method sets `useMediaDownloadService`. `+publishBatch` sends `multipart/mixed` to the document's `batchPath`. `--dry-run` prints the plan instead. |
|
|
61
|
+
| **Search** | `src/search.ts` | `search(models, query)`: SQLite FTS5 index (`node:sqlite`) built in memory per call from every loaded ApiModel, nothing on disk. Words split at camelCase, `-`, `_`, `.`; query tokens are letters and digits only, double-quoted, joined with `OR`; best 20 by `bm25`. |
|
|
62
|
+
| **Renderers** | `src/render.ts` | Root help, level listing, method help, search hits, `schemaView` (`$ref` expansion, cycle guard), `schemaFind` (`--find` field paths), and the skill reference Markdown that `pnpm gen:skill` writes to `skills/gapi/reference/` (generated; do not edit by hand). |
|
|
63
|
+
| **Response cache** | `src/responses.ts` | `--cache <age>` (1s–7d, opt-in per call, [ADR 0001](docs/adr/0001-response-cache.md)): `run` checks eligibility before any network call (no `nocache` entry, single call or pages, `GET` or a `reads` POST), then answers a hit from `responses/` in the config dir (dir 700, files 600) with no fetch, or stores a miss after a successful send. Key holds `identity(auth)` from Credentials (refresh-token hash), so accounts never share entries. `auth login` and `auth logout` delete `responses/`. |
|
|
64
|
+
| **Credentials** | `src/credentials.ts` | Only reader and writer of `config.json` and `credentials.json` (mode 600) in the config dir (`GAPI_CONFIG_DIR` or `~/.config/gapi`; it also holds the discovery `cache/` and `responses/`). `settings`: `--project`, `--api-key` and entry header flags, each flag → env → config. `authorize`: OAuth Bearer (refreshed token saved) or `X-Goog-Api-Key`. `login`, `status` (with `missing`), `logout` (revoke, then delete), `setup`. |
|
|
50
65
|
|
|
51
|
-
`
|
|
66
|
+
CLI tests call `run` in-process (`test/run.test.ts`, `test/helpers/io.ts`). Terms: [GLOSSARY.md](GLOSSARY.md).
|
|
52
67
|
|
|
53
68
|
## Where to look
|
|
54
69
|
|
|
55
70
|
| For | Read |
|
|
56
71
|
|---|---|
|
|
57
|
-
| Onboarding a Google account:
|
|
72
|
+
| Onboarding a Google account: services to enable, Ads access level, Business Profile access, API key, consent, Merchant registration, customer IDs, runtime errors | **[docs/SETUP.md](docs/SETUP.md)** |
|
|
73
|
+
| Using the CLI as an agent: grammar, `--params`, `schema`, `--dry-run`, paging, media, helpers, exit codes, marks | [skills/gapi/SKILL.md](skills/gapi/SKILL.md) + generated [reference](skills/gapi/reference/index.md) |
|
|
58
74
|
| What the CLI does, decisions, scopes, endpoints, risks | **[docs/SPEC.md](docs/SPEC.md)** — behavior source of truth |
|
|
59
75
|
| How work is tracked (issues, blocking edges) | [docs/agents/issue-tracker.md](docs/agents/issue-tracker.md) |
|
|
60
76
|
| Triage labels | [docs/agents/triage-labels.md](docs/agents/triage-labels.md) |
|
|
61
|
-
| Domain vocabulary (
|
|
77
|
+
| Domain vocabulary (GLOSSARY.md, ADRs) | [docs/agents/domain.md](docs/agents/domain.md) |
|
|
62
78
|
| Real commands and scripts | `package.json`, `gapi --help` |
|
|
63
79
|
|
|
64
80
|
## Add an API or a resource
|
|
65
81
|
|
|
66
|
-
- New
|
|
67
|
-
-
|
|
68
|
-
- Accept rule —
|
|
69
|
-
- Test through
|
|
82
|
+
- New API = one new `src/apis/<api>.ts` file that default-exports an API entry (`satisfies ApiEntry`): the discovery URL plus overlays (`retired`, `deprecated`, `paid`, `reads`, `nocache`, `headers`, `helpers`). The discovery document gives every resource and method. The glob finds the file; `api` must equal the file name, or the Catalog fails to load. No shared file changes.
|
|
83
|
+
- Check the API's caching terms; set `nocache` (`{ reason, source }`) when they forbid or bound caching ([ADR 0001](docs/adr/0001-response-cache.md)). Else list POST methods that only read (reports, searches) by id in `reads`, so `--cache` accepts them.
|
|
84
|
+
- Accept rule — `pnpm discovery:check` exits 0. It fails when an overlay names a method id that is not in the document. CI runs it weekly and on manual dispatch (`.github/workflows/discovery.yml`); `prepublishOnly` runs it before every publish.
|
|
85
|
+
- Test through `run(argv, io)`: fixture entries via `io.apisDir`, cache seeded in a temp `GAPI_CONFIG_DIR`, fake `fetch` (`test/helpers/fetch.ts`). `--dry-run` shows the request. Use no real network.
|
|
70
86
|
- Before commit: `pnpm typecheck` (tsc --noEmit) · `pnpm lint` (biome) · `pnpm test`.
|
|
71
87
|
- Fix lint by fixing the code it flags — never `biome --unsafe`, never disable a rule to clear it.
|
|
72
88
|
|
|
@@ -76,4 +92,4 @@ The project has 3 parts. A call moves through them in order.
|
|
|
76
92
|
|---|---|---|
|
|
77
93
|
| Issue tracker | GitHub issues via `gh` | [docs/agents/issue-tracker.md](docs/agents/issue-tracker.md) |
|
|
78
94
|
| Triage labels | 5 canonical labels (string == role) | [docs/agents/triage-labels.md](docs/agents/triage-labels.md) |
|
|
79
|
-
| Domain docs | Single-context (`
|
|
95
|
+
| Domain docs | Single-context (`GLOSSARY.md` + `docs/adr/`) | [docs/agents/domain.md](docs/agents/domain.md) |
|
package/dist/bin/gapi.js
CHANGED
|
@@ -1,138 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import { execute, pathParams } from '../src/rest.js';
|
|
13
|
-
const GLOBAL_OPTIONS = {
|
|
14
|
-
json: { type: 'boolean' },
|
|
15
|
-
raw: { type: 'boolean' },
|
|
16
|
-
help: { type: 'boolean' },
|
|
17
|
-
limit: { type: 'string' },
|
|
18
|
-
body: { type: 'string' },
|
|
19
|
-
project: { type: 'string' },
|
|
20
|
-
// Ads (see `ads gaql` sugar + resolveAdsHeaders): login-customer-id header,
|
|
21
|
-
// target account, and search/searchStream toggle.
|
|
22
|
-
'login-customer-id': { type: 'string' },
|
|
23
|
-
customer: { type: 'string' },
|
|
24
|
-
stream: { type: 'boolean' },
|
|
25
|
-
};
|
|
26
|
-
function printTree(manifests) {
|
|
27
|
-
console.log('gapi <api> <resource> <verb> [--json|--raw]\n');
|
|
28
|
-
console.log('auth setup | login [--manual] | status | logout\n');
|
|
29
|
-
for (const [api, m] of Object.entries(manifests).sort()) {
|
|
30
|
-
console.log(api);
|
|
31
|
-
for (const [resource, verbs] of Object.entries(m.resources).sort()) {
|
|
32
|
-
console.log(` ${resource} ${Object.keys(verbs).sort().join(' | ')}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
async function runAuth(sub, values) {
|
|
37
|
-
if (sub === 'login') {
|
|
38
|
-
const tokens = values.manual ? await loginManual() : await loginLoopback();
|
|
39
|
-
console.error('gapi: authorized (' +
|
|
40
|
-
(tokens.refresh_token ? 'refresh token stored' : 'no refresh token') +
|
|
41
|
-
')');
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (sub === 'setup') {
|
|
45
|
-
await configureAuth();
|
|
46
|
-
return console.error('gapi: saved to ~/.config/gapi/config.json');
|
|
47
|
-
}
|
|
48
|
-
if (sub === 'status')
|
|
49
|
-
return console.log(JSON.stringify(status(), null, 2));
|
|
50
|
-
if (sub === 'logout') {
|
|
51
|
-
logout();
|
|
52
|
-
return console.error('gapi: credentials removed');
|
|
53
|
-
}
|
|
54
|
-
throw new Error(`unknown auth command: ${sub}`);
|
|
55
|
-
}
|
|
56
|
-
async function main() {
|
|
57
|
-
const argv = process.argv.slice(2);
|
|
58
|
-
const manifests = await loadManifests();
|
|
59
|
-
checkSchema(manifests);
|
|
60
|
-
if (argv.length === 0 || (argv[0] === '--help' && argv.length === 1)) {
|
|
61
|
-
printTree(manifests);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const [api, ...rest] = argv;
|
|
65
|
-
if (api === 'auth') {
|
|
66
|
-
const { values, positionals } = parseArgs({
|
|
67
|
-
args: rest,
|
|
68
|
-
options: { manual: { type: 'boolean' }, help: { type: 'boolean' } },
|
|
69
|
-
allowPositionals: true,
|
|
70
|
-
});
|
|
71
|
-
return runAuth(positionals[0], values);
|
|
72
|
-
}
|
|
73
|
-
if (!manifests[api])
|
|
74
|
-
throw new Error(`unknown api: ${api} (try \`gapi --help\`)`);
|
|
75
|
-
const { values, positionals } = parseArgs({
|
|
76
|
-
args: rest,
|
|
77
|
-
options: GLOBAL_OPTIONS,
|
|
78
|
-
allowPositionals: true,
|
|
79
|
-
});
|
|
80
|
-
const [resource, verb] = positionals;
|
|
81
|
-
if (values.help || !resource || !verb) {
|
|
82
|
-
printTree({ [api]: manifests[api] });
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
// `ads gaql` is the one sugar beyond generic dispatch (story 31): the query is
|
|
86
|
-
// a positional, --stream picks searchStream, --customer fills the path param.
|
|
87
|
-
const isGaql = api === 'ads' && resource === 'gaql';
|
|
88
|
-
const row = isGaql
|
|
89
|
-
? findRow(manifests, api, 'gaql', values.stream ? 'searchStream' : 'search')
|
|
90
|
-
: findRow(manifests, api, resource, verb);
|
|
91
|
-
if (!row)
|
|
92
|
-
throw new Error(`unknown command: ${api} ${resource} ${verb}`);
|
|
93
|
-
const params = {};
|
|
94
|
-
if (isGaql) {
|
|
95
|
-
if (!values.customer)
|
|
96
|
-
throw new Error('ads gaql requires --customer <id>');
|
|
97
|
-
params.customer = values.customer;
|
|
98
|
-
params.body = gaqlBody(verb); // `verb` is the GAQL query string here
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
// params = positional path args after verb, then row.query params as further
|
|
102
|
-
// positionals, plus body/limit from flags.
|
|
103
|
-
const pp = pathParams(row.pathTemplate);
|
|
104
|
-
pp.forEach((name, i) => {
|
|
105
|
-
if (positionals[2 + i] != null)
|
|
106
|
-
params[name] = positionals[2 + i];
|
|
107
|
-
});
|
|
108
|
-
(row.query ?? []).forEach((name, i) => {
|
|
109
|
-
const v = positionals[2 + pp.length + i];
|
|
110
|
-
if (v != null)
|
|
111
|
-
params[name] = v;
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
if (values.limit != null)
|
|
115
|
-
params.limit = values.limit;
|
|
116
|
-
if (values.body != null)
|
|
117
|
-
params.body = JSON.parse(values.body);
|
|
118
|
-
const quotaProject = resolveSetting(values, {
|
|
119
|
-
flag: 'project',
|
|
120
|
-
env: QUOTA_PROJECT_ENV,
|
|
121
|
-
key: 'project',
|
|
122
|
-
});
|
|
123
|
-
// Only Ads calls carry the optional login-customer-id header, so other APIs
|
|
124
|
-
// never send an Ads-specific header.
|
|
125
|
-
const extraHeaders = api === 'ads' ? resolveAdsHeaders(values) : undefined;
|
|
126
|
-
const { json, raw } = await execute(row, params, {
|
|
127
|
-
tokenProvider: makeTokenProvider(),
|
|
128
|
-
fetch,
|
|
129
|
-
quotaProject,
|
|
130
|
-
extraHeaders,
|
|
2
|
+
import { run } from '../src/run.js';
|
|
3
|
+
const { argv, env, stdin, stdout, stderr } = process;
|
|
4
|
+
// stdout EPIPE (`gapi … | head`): the reader is gone, so stop, which also stops --page-all
|
|
5
|
+
// fetches. Any other stream error (ENOSPC, EBADF) lost output: exit 5, never a silent 0.
|
|
6
|
+
for (const s of [stdout, stderr]) {
|
|
7
|
+
s.on('error', (e) => {
|
|
8
|
+
if (e.code !== 'EPIPE')
|
|
9
|
+
process.exit(5);
|
|
10
|
+
if (s === stdout)
|
|
11
|
+
process.exit(0);
|
|
131
12
|
});
|
|
132
|
-
console.log(JSON.stringify(values.raw ? raw : json, null, 2));
|
|
133
13
|
}
|
|
134
|
-
|
|
135
|
-
const detail = e.body ? `\n${JSON.stringify(e.body.error ?? e.body, null, 2)}` : '';
|
|
136
|
-
console.error(`gapi: ${e.message}${detail}`);
|
|
137
|
-
process.exit(1);
|
|
138
|
-
});
|
|
14
|
+
process.exitCode = await run(argv.slice(2), { env, fetch, stdin, stdout, stderr });
|
package/dist/src/apis/ads.js
CHANGED
|
@@ -1,54 +1,64 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
// Google Ads API v26: the discovery entry, plus the `login-customer-id` header for MCC
|
|
2
|
+
// callers (D8) and the `+gaql` helper (D10). A version bump renames the resource literally
|
|
3
|
+
// named `v26` (`gapi ads v26 listPlannableLocations`), so its commands change with it.
|
|
4
|
+
// No developer-token header: it was sunset 2026-09-09 and the API ignores it
|
|
5
|
+
// (https://developers.google.com/google-ads/api/docs/api-policy/developer-token).
|
|
6
|
+
//
|
|
7
|
+
// `:search` keeps its raw {results, nextPageToken}; `ads customers googleAds search
|
|
8
|
+
// --page-all` pages it through the request body (epic #30 D6); `--stream` is the all-rows path.
|
|
9
|
+
import { GapiError } from '../errors.js';
|
|
10
|
+
import { isRecord } from '../json.js';
|
|
11
|
+
import { plan } from '../plan.js';
|
|
12
|
+
const GAQL_ARGS = '"<query>" --customer <id> [--stream]';
|
|
13
|
+
// searchStream (gRPC server-streaming) → over REST a JSON array of {results, ...} chunks
|
|
14
|
+
// with no nextPageToken; concat into one list. No Google doc covers an error mid-stream,
|
|
15
|
+
// so a chunk with an `error` key fails the call rather than vanish from the rows.
|
|
16
|
+
const streamConcat = (data) => {
|
|
17
|
+
if (!Array.isArray(data))
|
|
18
|
+
return data;
|
|
19
|
+
const failed = data.find((c) => isRecord(c) && Object.hasOwn(c, 'error'));
|
|
20
|
+
if (isRecord(failed)) {
|
|
21
|
+
const message = isRecord(failed.error) ? failed.error.message : undefined;
|
|
22
|
+
throw new GapiError(1, typeof message === 'string' ? message : 'searchStream: a chunk holds an error', failed);
|
|
23
|
+
}
|
|
24
|
+
const results = data.flatMap((c) => (isRecord(c) && Array.isArray(c.results) ? c.results : []));
|
|
25
|
+
// SUMMARY_ROW_SETTING puts the totals in the last chunk's summaryRow; keep them.
|
|
26
|
+
const summaryRow = data.findLast((c) => isRecord(c) && c.summaryRow !== undefined)?.summaryRow;
|
|
27
|
+
return summaryRow === undefined ? results : { results, summaryRow };
|
|
28
|
+
};
|
|
29
|
+
// D10: one GAQL query; `--stream` returns every row, without it the first page as returned.
|
|
30
|
+
const gaql = {
|
|
31
|
+
summary: `${GAQL_ARGS} run one GAQL query; --stream returns every row`,
|
|
32
|
+
options: { customer: { type: 'string' }, stream: { type: 'boolean' } },
|
|
33
|
+
plan: async ({ model, args, flags, settings }) => {
|
|
34
|
+
if (args.length !== 1)
|
|
35
|
+
throw new GapiError(3, `usage: gapi ads +gaql ${GAQL_ARGS}`);
|
|
36
|
+
if (typeof flags.customer !== 'string')
|
|
37
|
+
throw new GapiError(3, '--customer is required');
|
|
38
|
+
const stream = flags.stream === true;
|
|
39
|
+
const call = plan(model, `googleads.customers.googleAds.${stream ? 'searchStream' : 'search'}`, {
|
|
40
|
+
params: { customerId: flags.customer },
|
|
41
|
+
body: { query: args[0] },
|
|
42
|
+
dryRun: flags['dry-run'] === true,
|
|
43
|
+
}, settings);
|
|
44
|
+
return stream ? { ...call, decode: streamConcat } : call;
|
|
16
45
|
},
|
|
17
|
-
}
|
|
46
|
+
};
|
|
18
47
|
export default {
|
|
19
48
|
api: 'ads',
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
customers
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
49
|
+
discovery: 'https://googleads.googleapis.com/$discovery/rest?version=v26',
|
|
50
|
+
reads: [
|
|
51
|
+
'googleads.customers.googleAds.search',
|
|
52
|
+
'googleads.customers.googleAds.searchStream',
|
|
53
|
+
'googleads.googleAdsFields.search',
|
|
54
|
+
],
|
|
55
|
+
headers: [
|
|
56
|
+
{
|
|
57
|
+
name: 'login-customer-id',
|
|
58
|
+
flag: 'login-customer-id',
|
|
59
|
+
env: 'GOOGLE_ADS_LOGIN_CUSTOMER_ID',
|
|
60
|
+
configKey: 'login-customer-id',
|
|
30
61
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
search: {
|
|
34
|
-
httpMethod: 'POST',
|
|
35
|
-
pathTemplate: `${C}/googleAds:search`,
|
|
36
|
-
scopes: ADS,
|
|
37
|
-
},
|
|
38
|
-
// Streams the whole result set in one response (array of chunks).
|
|
39
|
-
searchStream: {
|
|
40
|
-
httpMethod: 'POST',
|
|
41
|
-
pathTemplate: `${C}/googleAds:searchStream`,
|
|
42
|
-
scopes: ADS,
|
|
43
|
-
decoder: streamConcat,
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
// Core write surface (:mutate). Body: {operations:[{create|update|remove}]}.
|
|
47
|
-
campaigns: mutate('campaigns'),
|
|
48
|
-
'campaign-budgets': mutate('campaignBudgets'),
|
|
49
|
-
'campaign-criteria': mutate('campaignCriteria'),
|
|
50
|
-
'ad-groups': mutate('adGroups'),
|
|
51
|
-
'ad-group-ads': mutate('adGroupAds'),
|
|
52
|
-
'ad-group-criteria': mutate('adGroupCriteria'),
|
|
53
|
-
},
|
|
62
|
+
],
|
|
63
|
+
helpers: { '+gaql': gaql },
|
|
54
64
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const SUNSET = 'https://developers.google.com/my-business/content/sunset-dates';
|
|
2
|
+
const health2024 = { reason: 'Discontinued 2024-07-01', source: SUNSET };
|
|
3
|
+
const qa2025 = { reason: 'Q&A API discontinued 2025-11-03', source: SUNSET };
|
|
4
|
+
const reference = { source: 'https://developers.google.com/my-business/reference/rest' };
|
|
5
|
+
export default {
|
|
6
|
+
api: 'business-v4',
|
|
7
|
+
discovery: 'https://developers.google.com/my-business/samples/mybusiness_google_rest_v4p9.json',
|
|
8
|
+
scopes: ['business.manage'],
|
|
9
|
+
retired: {
|
|
10
|
+
'mybusiness.accounts.locations.reportInsights': {
|
|
11
|
+
reason: 'Discontinued 2023-03-30',
|
|
12
|
+
source: SUNSET,
|
|
13
|
+
},
|
|
14
|
+
'mybusiness.accounts.locations.localPosts.reportInsights': {
|
|
15
|
+
reason: 'Discontinued 2023-02-20',
|
|
16
|
+
source: SUNSET,
|
|
17
|
+
},
|
|
18
|
+
'mybusiness.accounts.locations.insuranceNetworks.list': health2024,
|
|
19
|
+
'mybusiness.accounts.locations.getHealthProviderAttributes': health2024,
|
|
20
|
+
'mybusiness.accounts.locations.updateHealthProviderAttributes': health2024,
|
|
21
|
+
'mybusiness.accounts.locations.questions.create': qa2025,
|
|
22
|
+
'mybusiness.accounts.locations.questions.delete': qa2025,
|
|
23
|
+
'mybusiness.accounts.locations.questions.list': qa2025,
|
|
24
|
+
'mybusiness.accounts.locations.questions.patch': qa2025,
|
|
25
|
+
'mybusiness.accounts.locations.questions.answers.delete': qa2025,
|
|
26
|
+
'mybusiness.accounts.locations.questions.answers.list': qa2025,
|
|
27
|
+
'mybusiness.accounts.locations.questions.answers.upsert': qa2025,
|
|
28
|
+
},
|
|
29
|
+
// The reference page leaves accounts.listRecommendGoogleLocations unmarked: it stays live.
|
|
30
|
+
deprecated: {
|
|
31
|
+
'mybusiness.accounts.create': reference,
|
|
32
|
+
'mybusiness.accounts.deleteNotifications': reference,
|
|
33
|
+
'mybusiness.accounts.generateAccountNumber': reference,
|
|
34
|
+
'mybusiness.accounts.get': reference,
|
|
35
|
+
'mybusiness.accounts.getNotifications': reference,
|
|
36
|
+
'mybusiness.accounts.list': reference,
|
|
37
|
+
'mybusiness.accounts.update': reference,
|
|
38
|
+
'mybusiness.accounts.updateNotifications': reference,
|
|
39
|
+
'mybusiness.accounts.admins.create': reference,
|
|
40
|
+
'mybusiness.accounts.admins.delete': reference,
|
|
41
|
+
'mybusiness.accounts.admins.list': reference,
|
|
42
|
+
'mybusiness.accounts.admins.patch': reference,
|
|
43
|
+
'mybusiness.accounts.invitations.accept': reference,
|
|
44
|
+
'mybusiness.accounts.invitations.decline': reference,
|
|
45
|
+
'mybusiness.accounts.invitations.list': reference,
|
|
46
|
+
'mybusiness.accounts.locations.admins.create': reference,
|
|
47
|
+
'mybusiness.accounts.locations.admins.delete': reference,
|
|
48
|
+
'mybusiness.accounts.locations.admins.list': reference,
|
|
49
|
+
'mybusiness.accounts.locations.admins.patch': reference,
|
|
50
|
+
'mybusiness.accounts.locations.transfer': reference,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
api: 'crux',
|
|
3
|
+
discovery: 'https://chromeuxreport.googleapis.com/$discovery/rest?version=v1',
|
|
4
|
+
nocache: {
|
|
5
|
+
reason: 'Google-owned content; Google APIs Terms §5 bound caching to the cache header',
|
|
6
|
+
source: 'https://developers.google.com/terms',
|
|
7
|
+
},
|
|
8
|
+
};
|