google-apis-cli 0.1.0 → 0.2.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.
Files changed (67) hide show
  1. package/README.md +39 -26
  2. package/dist/bin/gapi.js +6 -137
  3. package/dist/src/apis/ads.js +51 -49
  4. package/dist/src/apis/bid-manager.js +4 -0
  5. package/dist/src/apis/bigquery-dts.js +4 -0
  6. package/dist/src/apis/business-account-management.js +5 -0
  7. package/dist/src/apis/business-information.js +5 -0
  8. package/dist/src/apis/business-lodging.js +5 -0
  9. package/dist/src/apis/business-notifications.js +5 -0
  10. package/dist/src/apis/business-performance.js +5 -0
  11. package/dist/src/apis/business-place-actions.js +5 -0
  12. package/dist/src/apis/business-v4.js +52 -0
  13. package/dist/src/apis/business-verifications.js +5 -0
  14. package/dist/src/apis/cm360.js +4 -0
  15. package/dist/src/apis/crux.js +4 -0
  16. package/dist/src/apis/data-manager.js +4 -0
  17. package/dist/src/apis/dv360.js +4 -0
  18. package/dist/src/apis/ga4-admin-alpha.js +4 -0
  19. package/dist/src/apis/ga4-admin.js +1 -288
  20. package/dist/src/apis/ga4-data-alpha.js +4 -0
  21. package/dist/src/apis/ga4-data.js +1 -69
  22. package/dist/src/apis/gsc.js +5 -58
  23. package/dist/src/apis/gtm.js +1 -189
  24. package/dist/src/apis/indexing.js +37 -15
  25. package/dist/src/apis/merchant-accounts.js +4 -0
  26. package/dist/src/apis/merchant-conversions.js +4 -0
  27. package/dist/src/apis/merchant-datasources.js +4 -0
  28. package/dist/src/apis/merchant-inventories.js +4 -0
  29. package/dist/src/apis/merchant-issueresolution.js +4 -0
  30. package/dist/src/apis/merchant-lfp.js +4 -0
  31. package/dist/src/apis/merchant-loyaltycustomers.js +4 -0
  32. package/dist/src/apis/merchant-notifications.js +4 -0
  33. package/dist/src/apis/merchant-ordertracking.js +4 -0
  34. package/dist/src/apis/merchant-products.js +4 -0
  35. package/dist/src/apis/merchant-promotions.js +4 -0
  36. package/dist/src/apis/merchant-quota.js +4 -0
  37. package/dist/src/apis/merchant-reports.js +4 -0
  38. package/dist/src/apis/merchant-reviews.js +4 -0
  39. package/dist/src/apis/pagespeed.js +4 -0
  40. package/dist/src/apis/places.js +15 -0
  41. package/dist/src/apis/postmaster.js +4 -0
  42. package/dist/src/apis/sa360-legacy.js +4 -0
  43. package/dist/src/apis/sa360.js +12 -0
  44. package/dist/src/apis/sheets.js +4 -0
  45. package/dist/src/apis/site-verification.js +4 -0
  46. package/dist/src/apis/web-risk.js +14 -0
  47. package/dist/src/apis/youtube-analytics.js +4 -0
  48. package/dist/src/apis/youtube-reporting.js +4 -0
  49. package/dist/src/apis/youtube.js +4 -0
  50. package/dist/src/catalog.js +203 -0
  51. package/dist/src/credentials.js +316 -0
  52. package/dist/src/errors.js +16 -0
  53. package/dist/src/json.js +11 -0
  54. package/dist/src/plan.js +315 -0
  55. package/dist/src/render.js +183 -0
  56. package/dist/src/run.js +263 -0
  57. package/dist/src/transport/batch.js +75 -0
  58. package/dist/src/transport/download.js +15 -0
  59. package/dist/src/transport/upload.js +81 -0
  60. package/dist/src/transport.js +99 -0
  61. package/dist/src/types.js +1 -13
  62. package/package.json +22 -7
  63. package/dist/src/apis/ga4-admin-v1alpha.js +0 -280
  64. package/dist/src/auth.js +0 -172
  65. package/dist/src/config.js +0 -89
  66. package/dist/src/manifest.js +0 -69
  67. 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 Ads, GTM, GA4 (Admin + Data), Search Console, and Indexing.
3
+ `gapi` is one CLI for 47 Google API entries, built from Google's discovery documents at runtime:
4
4
 
5
- - One login. One refresh token. All scopes. Read and write.
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 24. Node runs `.ts` directly (type-stripping) — no build step in dev. Biome for lint + format. pnpm 12 (corepack-pinned) for package management.
17
+ - TypeScript 7 (strict), Node 24. 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> <verb> [--json|--raw]`
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>]] [--dry-run]`. `gapi <api> [<resource>…]` lists a level (the api level also lists helpers); `… <method> --help` shows method help. `gapi <api> +<helper> … [--dry-run] [--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 schema <api> <resource>[.<sub>…].<method> | <SchemaName> [--resolve-refs]` prints a method shape or a schema as JSON. `gapi discovery refresh [<api>…]` refetches discovery documents now; `gapi discovery path` prints the cache dir. `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,61 @@ 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 ≥ 24** — `gapi` ships as TypeScript and runs on Node's native type-stripping, so there is no build step and no bundle.
28
+ Needs **Node ≥ 24**. 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, all 13 scopes
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 five steps and
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 every ~7 days — re-run `gapi auth login` when it does.
37
- - **Production** removes that expiry, but Google requires **OAuth App Verification** first: a privacy-policy URL, a homepage, and a reviewed consent screen. Review timeline is not guaranteed.
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; Production is the no-expiry target. Until verification passes, stay in Testing and re-auth on the ~7-day cadence.
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
- The project has 3 parts. A call moves through them in order.
53
+ A call moves through these parts in order.
44
54
 
45
55
  | Part | File | Function |
46
56
  |---|---|---|
47
- | **auth** | `src/auth.ts` | `gapi auth login` gets one OAuth consent. It stores the refresh token in `~/.config/gapi/credentials.json` (mode 600). `getAccessToken()` refreshes the access token. Only part with real handlers. |
48
- | **manifest** | `src/apis/*.ts` | Each API is data, not code: `resource → verb → { httpMethod, pathTemplate, scopes, requiredHeaders?, decoder?, listKey?, query? }`. One file per surface. A glob finds them. No central registry. Row/seam types live in `src/types.ts`. |
49
- | **executor seam** | `src/rest.ts` | `execute(manifestRow, params, { tokenProvider, fetch })` builds the URL, adds headers, follows pagination, applies the `decoder`, and formats `--json`/`--raw`. All network calls pass here. It is the one test seam. |
57
+ | **run** | `src/run.ts` | `run(argv, io)`: strict argv parse, routing (`--help`, `auth`, `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` | `send(plan, { fetch, authorize, out })`. `authorize` adds the credential per request; non-2xx → exit 1; 403 `ACCESS_TOKEN_SCOPE_INSUFFICIENT` → 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); `--output` writes the `alt=media` body to a file, from `rootUrl` + `download/` when the method sets `useMediaDownloadService`. `--dry-run` prints the plan instead. |
61
+ | **Renderers** | `src/render.ts` | Root help, level listing, method help, `schemaView` (`$ref` expansion, cycle guard), and the skill reference Markdown that `pnpm gen:skill` writes to `skills/gapi/reference/` (generated; do not edit by hand). |
62
+ | **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`). `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
63
 
51
- `bin/gapi.ts` parses argv. The dispatch, `--help`, and URL resolution all come from the manifests.
64
+ CLI tests call `run` in-process (`test/run.test.ts`, `test/helpers/io.ts`). Terms: [GLOSSARY.md](GLOSSARY.md).
52
65
 
53
66
  ## Where to look
54
67
 
55
68
  | For | Read |
56
69
  |---|---|
57
- | Onboarding a Google account: consent, Ads access level, customer IDs, runtime errors | **[docs/SETUP.md](docs/SETUP.md)** |
70
+ | 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)** |
71
+ | 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
72
  | What the CLI does, decisions, scopes, endpoints, risks | **[docs/SPEC.md](docs/SPEC.md)** — behavior source of truth |
59
73
  | How work is tracked (issues, blocking edges) | [docs/agents/issue-tracker.md](docs/agents/issue-tracker.md) |
60
74
  | Triage labels | [docs/agents/triage-labels.md](docs/agents/triage-labels.md) |
61
- | Domain vocabulary (CONTEXT.md, ADRs) | [docs/agents/domain.md](docs/agents/domain.md) |
75
+ | Domain vocabulary (GLOSSARY.md, ADRs) | [docs/agents/domain.md](docs/agents/domain.md) |
62
76
  | Real commands and scripts | `package.json`, `gapi --help` |
63
77
 
64
78
  ## Add an API or a resource
65
79
 
66
- - New resource = one manifest row.
67
- - New API = one new `src/apis/<api>.ts` file (default-export `satisfies Manifest`). The glob finds it. No shared file changes.
68
- - Accept rule — **coverage-oracle**: the self-check fails if the manifest does not cover 100% of the discovery-doc resources.
69
- - Test through the executor seam. Inject a fake `tokenProvider` and `fetch`. Use no real network.
80
+ - 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`, `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.
81
+ - 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.
82
+ - 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
83
  - Before commit: `pnpm typecheck` (tsc --noEmit) · `pnpm lint` (biome) · `pnpm test`.
71
84
  - Fix lint by fixing the code it flags — never `biome --unsafe`, never disable a rule to clear it.
72
85
 
@@ -76,4 +89,4 @@ The project has 3 parts. A call moves through them in order.
76
89
  |---|---|---|
77
90
  | Issue tracker | GitHub issues via `gh` | [docs/agents/issue-tracker.md](docs/agents/issue-tracker.md) |
78
91
  | Triage labels | 5 canonical labels (string == role) | [docs/agents/triage-labels.md](docs/agents/triage-labels.md) |
79
- | Domain docs | Single-context (`CONTEXT.md` + `docs/adr/`) | [docs/agents/domain.md](docs/agents/domain.md) |
92
+ | 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,7 @@
1
1
  #!/usr/bin/env node
2
- // argv -> dispatch. The command tree, --help, and URL resolution all derive
3
- // from the auto-discovered manifests (single source). Two branches step outside
4
- // generic dispatch: `auth` (real handlers) and the `ads gaql` sugar (story 31,
5
- // query positional + --customer/--stream). Ads adds the optional
6
- // login-customer-id HTTP header (resolveAdsHeaders) on `ads` calls only.
7
- import { parseArgs } from 'node:util';
8
- import { gaqlBody } from '../src/apis/ads.js';
9
- import { configureAuth, loginLoopback, loginManual, logout, makeTokenProvider, status, } from '../src/auth.js';
10
- import { QUOTA_PROJECT_ENV, resolveAdsHeaders, resolveSetting } from '../src/config.js';
11
- import { checkSchema, findRow, loadManifests } from '../src/manifest.js';
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,
131
- });
132
- console.log(JSON.stringify(values.raw ? raw : json, null, 2));
133
- }
134
- main().catch((e) => {
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
- });
2
+ import { run } from '../src/run.js';
3
+ const { argv, env, stdin, stdout, stderr } = process;
4
+ // Ignore stream errors (EPIPE on `gapi … | head`), as console.log did before run(argv, io).
5
+ for (const s of [stdout, stderr])
6
+ s.on('error', () => { });
7
+ process.exitCode = await run(argv.slice(2), { env, fetch, stdin, stdout, stderr });
@@ -1,54 +1,56 @@
1
- const ADS = ['adwords'];
2
- const C = '/v25/customers/{customer}';
3
- // Build the search-request body from a bare GAQL string (story 31). Trivial by
4
- // design — kept a named pure function so bin/gapi.ts's `gapi ads gaql "<q>"`
5
- // sugar has one tested seam.
6
- export const gaqlBody = (query) => ({ query });
7
- // searchStream → JSON array of {results, ...} chunks; concat into one list.
8
- const streamConcat = (data) => Array.isArray(data) ? data.flatMap((c) => c?.results ?? []) : data;
9
- // Every :mutate service is the same shape: POST customers/{}/{collection}:mutate
10
- // with a {operations:[...]} body. Only the collection segment varies.
11
- const mutate = (collection) => ({
12
- mutate: {
13
- httpMethod: 'POST',
14
- pathTemplate: `${C}/${collection}:mutate`,
15
- scopes: ADS,
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
+ return data.flatMap((c) => (isRecord(c) && Array.isArray(c.results) ? c.results : []));
25
+ };
26
+ // D10: one GAQL query; `--stream` returns every row, without it the first page as returned.
27
+ const gaql = {
28
+ summary: `${GAQL_ARGS} run one GAQL query; --stream returns every row`,
29
+ options: { customer: { type: 'string' }, stream: { type: 'boolean' } },
30
+ plan: async ({ model, args, flags, settings }) => {
31
+ if (args.length !== 1)
32
+ throw new GapiError(3, `usage: gapi ads +gaql ${GAQL_ARGS}`);
33
+ if (typeof flags.customer !== 'string')
34
+ throw new GapiError(3, '--customer is required');
35
+ const stream = flags.stream === true;
36
+ const call = plan(model, `googleads.customers.googleAds.${stream ? 'searchStream' : 'search'}`, {
37
+ params: { customerId: flags.customer },
38
+ body: { query: args[0] },
39
+ dryRun: flags['dry-run'] === true,
40
+ }, settings);
41
+ return stream ? { ...call, decode: streamConcat } : call;
16
42
  },
17
- });
43
+ };
18
44
  export default {
19
45
  api: 'ads',
20
- baseUrl: 'https://googleads.googleapis.com',
21
- resources: {
22
- customers: {
23
- // Empty body; returns { resourceNames: ["customers/123", ...] }. Ignores
24
- // login-customer-id (direct-access accounts only).
25
- 'list-accessible': {
26
- httpMethod: 'GET',
27
- pathTemplate: '/v25/customers:listAccessibleCustomers',
28
- scopes: ADS,
29
- },
46
+ discovery: 'https://googleads.googleapis.com/$discovery/rest?version=v26',
47
+ headers: [
48
+ {
49
+ name: 'login-customer-id',
50
+ flag: 'login-customer-id',
51
+ env: 'GOOGLE_ADS_LOGIN_CUSTOMER_ID',
52
+ configKey: 'login-customer-id',
30
53
  },
31
- gaql: {
32
- // Body-paged: {query, pageToken}. Raw {results, nextPageToken} out.
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
- },
54
+ ],
55
+ helpers: { '+gaql': gaql },
54
56
  };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'bid-manager',
3
+ discovery: 'https://doubleclickbidmanager.googleapis.com/$discovery/rest?version=v2',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'bigquery-dts',
3
+ discovery: 'https://bigquerydatatransfer.googleapis.com/$discovery/rest?version=v1',
4
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ api: 'business-account-management',
3
+ discovery: 'https://mybusinessaccountmanagement.googleapis.com/$discovery/rest?version=v1',
4
+ scopes: ['business.manage'],
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ api: 'business-information',
3
+ discovery: 'https://mybusinessbusinessinformation.googleapis.com/$discovery/rest?version=v1',
4
+ scopes: ['business.manage'],
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ api: 'business-lodging',
3
+ discovery: 'https://mybusinesslodging.googleapis.com/$discovery/rest?version=v1',
4
+ scopes: ['business.manage'],
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ api: 'business-notifications',
3
+ discovery: 'https://mybusinessnotifications.googleapis.com/$discovery/rest?version=v1',
4
+ scopes: ['business.manage'],
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ api: 'business-performance',
3
+ discovery: 'https://businessprofileperformance.googleapis.com/$discovery/rest?version=v1',
4
+ scopes: ['business.manage'],
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ api: 'business-place-actions',
3
+ discovery: 'https://mybusinessplaceactions.googleapis.com/$discovery/rest?version=v1',
4
+ scopes: ['business.manage'],
5
+ };
@@ -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,5 @@
1
+ export default {
2
+ api: 'business-verifications',
3
+ discovery: 'https://mybusinessverifications.googleapis.com/$discovery/rest?version=v1',
4
+ scopes: ['business.manage'],
5
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'cm360',
3
+ discovery: 'https://dfareporting.googleapis.com/$discovery/rest?version=v5',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'crux',
3
+ discovery: 'https://chromeuxreport.googleapis.com/$discovery/rest?version=v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'data-manager',
3
+ discovery: 'https://datamanager.googleapis.com/$discovery/rest?version=v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'dv360',
3
+ discovery: 'https://displayvideo.googleapis.com/$discovery/rest?version=v4',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'ga4-admin-alpha',
3
+ discovery: 'https://analyticsadmin.googleapis.com/$discovery/rest?version=v1alpha',
4
+ };