@stravica-ai/digital-operator-pa 0.1.17 → 0.1.19
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/dist/engineering-pa-cli/docs/gsc.md +22 -2
- package/dist/engineering-pa-cli/src/gsc/auth.mjs +37 -15
- package/dist/engineering-pa-cli/src/gsc/cli.mjs +8 -0
- package/dist/engineering-pa-cli/src/gsc/cloudflare.mjs +16 -7
- package/dist/engineering-pa-cli/src/gsc/commands/sitemap-status.mjs +95 -0
- package/dist/engineering-pa-cli/src/pa/cli.mjs +1 -1
- package/package.json +1 -1
|
@@ -6,8 +6,9 @@ domain whose DNS lives on Cloudflare in an account the operator controls.
|
|
|
6
6
|
## Status
|
|
7
7
|
|
|
8
8
|
Implemented: `add-property`, `verify-via-cf-dns`, `submit-sitemap`,
|
|
9
|
-
`pull-analytics`, `list-properties`. Auth via Google
|
|
10
|
-
loaded from the SOPS vault. Cloudflare planting via
|
|
9
|
+
`sitemap-status`, `pull-analytics`, `list-properties`. Auth via Google
|
|
10
|
+
service-account JSON loaded from the SOPS vault. Cloudflare planting via
|
|
11
|
+
the CF v4 REST API.
|
|
11
12
|
|
|
12
13
|
## Dependencies
|
|
13
14
|
|
|
@@ -37,10 +38,20 @@ Two npm packages (declared in `@stravica-ai/digital-operator-pa`):
|
|
|
37
38
|
|
|
38
39
|
5. **Store the SA key in the SOPS vault**:
|
|
39
40
|
```bash
|
|
41
|
+
# Legacy single-file vault:
|
|
40
42
|
pnpm exec secrets set GSC_SERVICE_ACCOUNT_KEY -
|
|
43
|
+
# Scoped-vault estate (workspace or canonical layout):
|
|
44
|
+
pnpm exec secrets set --scope <scope> GSC_SERVICE_ACCOUNT_KEY -
|
|
41
45
|
# paste the entire JSON contents, then Ctrl-D
|
|
42
46
|
```
|
|
43
47
|
|
|
48
|
+
On scoped-vault hosts, `pa gsc` reads the key from whichever pot
|
|
49
|
+
`secrets get` would use — i.e. it honours (in order) `SECRETS_DEFAULT_SCOPE`,
|
|
50
|
+
the `defaultScope` in `~/.config/stravica-pa/defaults.json`, or the
|
|
51
|
+
calling PA's `identity.json` — before falling back to the legacy
|
|
52
|
+
single-file vault. Store the key in the pot the host is already
|
|
53
|
+
configured to read; no `pa gsc`-specific configuration is needed.
|
|
54
|
+
|
|
44
55
|
6. **Store Cloudflare credentials** (also one-time). Two key shapes are
|
|
45
56
|
supported; pick whichever fits your setup:
|
|
46
57
|
|
|
@@ -70,6 +81,10 @@ Two npm packages (declared in `@stravica-ai/digital-operator-pa`):
|
|
|
70
81
|
no `--account` flag is passed and the `CLOUDFLARE_PAGES_*` unsuffixed
|
|
71
82
|
pair is absent. Don't rely on it — set the `CLOUDFLARE_PAGES_*` keys.
|
|
72
83
|
|
|
84
|
+
Same scoped-vault note as the SA key: on scoped hosts these live in
|
|
85
|
+
whichever pot `secrets get` reads (see step 5); use `secrets set --scope
|
|
86
|
+
<scope> ...` to store them there.
|
|
87
|
+
|
|
73
88
|
## Per-domain setup (each time you onboard a new property)
|
|
74
89
|
|
|
75
90
|
7. **Grant the service-account access to the GSC property.** Until the
|
|
@@ -104,6 +119,11 @@ pnpm exec pa gsc verify-via-cf-dns besafetraining.co.uk --account clive
|
|
|
104
119
|
# Submit a sitemap
|
|
105
120
|
pnpm exec pa gsc submit-sitemap stravica.ai https://stravica.ai/sitemap.xml
|
|
106
121
|
|
|
122
|
+
# Read sitemap state (list every sitemap the property has)
|
|
123
|
+
pnpm exec pa gsc sitemap-status stravica.ai
|
|
124
|
+
# Read state for one named sitemap
|
|
125
|
+
pnpm exec pa gsc sitemap-status stravica.ai https://stravica.ai/sitemap.xml
|
|
126
|
+
|
|
107
127
|
# Pull search analytics (default 28d, top queries, 100 rows)
|
|
108
128
|
pnpm exec pa gsc pull-analytics stravica.ai
|
|
109
129
|
pnpm exec pa gsc pull-analytics stravica.ai --days 90 --dimensions query,page --rows 500
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// override and never touch the network. The SA-key read also accepts an
|
|
9
9
|
// override so tests can pass in a synthetic key without going through SOPS.
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { resolveCommandPaths, checkVault, secretsFileExists, sopsDecrypt, VaultError } from '../secrets/vault.mjs';
|
|
12
12
|
|
|
13
13
|
// Read-only scope: searchanalytics.query + sites.list + sitemaps.list.
|
|
14
14
|
// Read-write scope: sites.add, sitemaps.submit, site verification.
|
|
@@ -25,9 +25,26 @@ const DEFAULT_VAULT_KEY = 'GSC_SERVICE_ACCOUNT_KEY';
|
|
|
25
25
|
/**
|
|
26
26
|
* Load the service-account JSON object from the SOPS vault.
|
|
27
27
|
*
|
|
28
|
+
* Vault path resolution goes through `resolveCommandPaths` — the same
|
|
29
|
+
* resolver `secrets get`/`set` use — so this honours all the same layouts
|
|
30
|
+
* without duplicating logic: an explicit `overrides.paths` (from a
|
|
31
|
+
* pre-resolved `--scope`/`--file`) is a pass-through; otherwise the
|
|
32
|
+
* default-scope resolver runs (SECRETS_DEFAULT_SCOPE env, defaultScope in
|
|
33
|
+
* ~/.config/stravica-pa/defaults.json, or the calling PA's identity.json)
|
|
34
|
+
* and points at a scoped `.vault/scopes/<scope>.enc.json`; if nothing is
|
|
35
|
+
* configured anywhere it falls back to the legacy single-file vault.
|
|
36
|
+
*
|
|
37
|
+
* A stderr `note: writing to '<scope>' pot ...` line is printed by the
|
|
38
|
+
* resolver on the default-scope path — same behaviour as `secrets get`,
|
|
39
|
+
* kept intentionally consistent so operators see the same routing signal
|
|
40
|
+
* from every consumer of the vault.
|
|
41
|
+
*
|
|
28
42
|
* @param {object} overrides
|
|
29
|
-
* @param {object} [overrides.paths] Vault paths (from
|
|
43
|
+
* @param {object} [overrides.paths] Vault paths already resolved (e.g. from --scope upstream).
|
|
30
44
|
* @param {string} [overrides.vaultKey] Vault key name (default GSC_SERVICE_ACCOUNT_KEY).
|
|
45
|
+
* @param {string} [overrides.cwd] Test seam — cwd used for workspace resolution.
|
|
46
|
+
* @param {object} [overrides.env] Test seam — env used for scope-default resolution.
|
|
47
|
+
* @param {string} [overrides.home] Test seam — home used for keyfile + defaults.json lookup.
|
|
31
48
|
* @param {string} [overrides.serviceAccountKey]
|
|
32
49
|
* Raw SA JSON string. If provided, skips vault entirely (testing).
|
|
33
50
|
* @returns {Promise<object>} Parsed service-account JSON.
|
|
@@ -39,34 +56,39 @@ export async function loadServiceAccountKey(overrides = {}) {
|
|
|
39
56
|
: overrides.serviceAccountKey;
|
|
40
57
|
}
|
|
41
58
|
|
|
42
|
-
const paths = overrides
|
|
59
|
+
const paths = resolveCommandPaths(overrides);
|
|
43
60
|
const vaultKey = overrides.vaultKey || DEFAULT_VAULT_KEY;
|
|
44
61
|
|
|
45
62
|
const state = checkVault(paths);
|
|
46
63
|
if (!state.ok) {
|
|
64
|
+
// Print then throw — main() suppresses messages for errors that carry
|
|
65
|
+
// an exitCode (the throw-site is expected to have reported), so
|
|
66
|
+
// omitting the console.error would exit silently through the pa gsc
|
|
67
|
+
// binary.
|
|
68
|
+
console.error(state.message);
|
|
47
69
|
throw new VaultError(state.message, state.exitCode ?? 1);
|
|
48
70
|
}
|
|
49
71
|
if (!secretsFileExists(paths)) {
|
|
50
|
-
|
|
51
|
-
`
|
|
52
|
-
|
|
53
|
-
);
|
|
72
|
+
const message = `No secrets file at ${paths.secretsFile}.` + (paths.scoped
|
|
73
|
+
? ` Store the service-account JSON with:\n secrets set --scope ${paths.scope} ${vaultKey} -\n(then paste the JSON, then Ctrl-D)`
|
|
74
|
+
: " Run 'secrets init' first.");
|
|
75
|
+
console.error(message);
|
|
76
|
+
throw new VaultError(message, 1);
|
|
54
77
|
}
|
|
55
78
|
const data = await sopsDecrypt(paths, paths.secretsFile);
|
|
56
79
|
if (!(vaultKey in data)) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
);
|
|
80
|
+
const scopeHint = paths.scoped ? ` --scope ${paths.scope}` : '';
|
|
81
|
+
const message = `Vault key "${vaultKey}" not found in ${paths.secretsFile}. Store the service-account JSON with:\n secrets set${scopeHint} ${vaultKey} -\n(then paste the JSON, then Ctrl-D)`;
|
|
82
|
+
console.error(message);
|
|
83
|
+
throw new VaultError(message, 1);
|
|
61
84
|
}
|
|
62
85
|
const raw = data[vaultKey];
|
|
63
86
|
try {
|
|
64
87
|
return typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
65
88
|
} catch (e) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
89
|
+
const message = `Vault key "${vaultKey}" is not valid JSON: ${e.message}`;
|
|
90
|
+
console.error(message);
|
|
91
|
+
throw new VaultError(message, 2);
|
|
70
92
|
}
|
|
71
93
|
}
|
|
72
94
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { runAddProperty } from './commands/add-property.mjs';
|
|
10
10
|
import { runVerifyViaCfDns } from './commands/verify-via-cf-dns.mjs';
|
|
11
11
|
import { runSubmitSitemap } from './commands/submit-sitemap.mjs';
|
|
12
|
+
import { runSitemapStatus } from './commands/sitemap-status.mjs';
|
|
12
13
|
import { runPullAnalytics } from './commands/pull-analytics.mjs';
|
|
13
14
|
import { runListProperties } from './commands/list-properties.mjs';
|
|
14
15
|
|
|
@@ -29,6 +30,12 @@ Usage:
|
|
|
29
30
|
pa gsc submit-sitemap <domain> <sitemap-url>
|
|
30
31
|
Submit a sitemap to GSC for the property.
|
|
31
32
|
|
|
33
|
+
pa gsc sitemap-status <domain> [sitemap-url]
|
|
34
|
+
Read sitemap state for the property (sitemaps.list) or one named
|
|
35
|
+
sitemap (sitemaps.get). Reports path, lastSubmitted, lastDownloaded,
|
|
36
|
+
isPending, warnings, errors, and per-type contents[] submitted/indexed
|
|
37
|
+
counts. Read-only.
|
|
38
|
+
|
|
32
39
|
pa gsc pull-analytics <domain> [--days N] [--dimensions D1,D2,...] [--rows N]
|
|
33
40
|
Pull search-analytics rows. Defaults: --days 28 --dimensions query
|
|
34
41
|
--rows 100. Outputs a JSON array on stdout.
|
|
@@ -69,6 +76,7 @@ export async function run(argv, overrides = {}) {
|
|
|
69
76
|
case 'add-property': return runAddProperty(rest, overrides);
|
|
70
77
|
case 'verify-via-cf-dns': return runVerifyViaCfDns(rest, overrides);
|
|
71
78
|
case 'submit-sitemap': return runSubmitSitemap(rest, overrides);
|
|
79
|
+
case 'sitemap-status': return runSitemapStatus(rest, overrides);
|
|
72
80
|
case 'pull-analytics': return runPullAnalytics(rest, overrides);
|
|
73
81
|
case 'list-properties': return runListProperties(rest, overrides);
|
|
74
82
|
default: {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
//
|
|
25
25
|
// `fetchImpl` and `now` are injectable for tests.
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import { resolveCommandPaths, checkVault, secretsFileExists, sopsDecrypt, VaultError } from '../secrets/vault.mjs';
|
|
28
28
|
|
|
29
29
|
const CF_API = 'https://api.cloudflare.com/client/v4';
|
|
30
30
|
|
|
@@ -122,14 +122,23 @@ export async function loadCloudflareCreds(overrides = {}) {
|
|
|
122
122
|
if (overrides.vaultData) {
|
|
123
123
|
data = overrides.vaultData;
|
|
124
124
|
} else {
|
|
125
|
-
|
|
125
|
+
// resolveCommandPaths honours --scope/--file (via a pre-resolved
|
|
126
|
+
// overrides.paths), the SECRETS_DEFAULT_SCOPE env, defaults.json's
|
|
127
|
+
// defaultScope, and the calling PA's identity.json, before falling
|
|
128
|
+
// back to the legacy single-file vault. Same routing as `secrets get`
|
|
129
|
+
// and `pa gsc`'s SA-key loader — one resolver, one routing signal.
|
|
130
|
+
const paths = resolveCommandPaths(overrides);
|
|
126
131
|
const state = checkVault(paths);
|
|
127
|
-
if (!state.ok)
|
|
132
|
+
if (!state.ok) {
|
|
133
|
+
console.error(state.message);
|
|
134
|
+
throw new VaultError(state.message, state.exitCode ?? 1);
|
|
135
|
+
}
|
|
128
136
|
if (!secretsFileExists(paths)) {
|
|
129
|
-
|
|
130
|
-
`
|
|
131
|
-
|
|
132
|
-
);
|
|
137
|
+
const message = `No secrets file at ${paths.secretsFile}.` + (paths.scoped
|
|
138
|
+
? ` Store Cloudflare credentials with 'secrets set --scope ${paths.scope} <KEY> <value>'.`
|
|
139
|
+
: " Run 'secrets init' first.");
|
|
140
|
+
console.error(message);
|
|
141
|
+
throw new VaultError(message, 1);
|
|
133
142
|
}
|
|
134
143
|
data = await sopsDecrypt(paths, paths.secretsFile);
|
|
135
144
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// sitemap-status — Read sitemap state for a GSC property.
|
|
2
|
+
//
|
|
3
|
+
// Wraps webmasters.sitemaps.list (no sitemap-url) and
|
|
4
|
+
// webmasters.sitemaps.get (with sitemap-url). Both are read-only.
|
|
5
|
+
//
|
|
6
|
+
// Every previous "did the release-time submit take?" check was a one-off
|
|
7
|
+
// workaround — this verb makes it a first-class subcommand so the pattern
|
|
8
|
+
// is repeatable and auditable.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// pa gsc sitemap-status <domain>
|
|
12
|
+
// List every sitemap the property has, with lastSubmitted,
|
|
13
|
+
// lastDownloaded, isPending, warnings, errors, and per-type
|
|
14
|
+
// contents[] submitted/indexed counts.
|
|
15
|
+
//
|
|
16
|
+
// pa gsc sitemap-status <domain> <sitemap-url>
|
|
17
|
+
// Same shape, but only for the named sitemap (webmasters.sitemaps.get).
|
|
18
|
+
|
|
19
|
+
import { buildGscClient, toDomainProperty } from '../auth.mjs';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalise a Sitemap resource from the GSC API into a stable shape.
|
|
23
|
+
* The GSC API returns warnings/errors/submitted/indexed as strings; we
|
|
24
|
+
* keep them as strings (matches the wire format so operators reading the
|
|
25
|
+
* JSON see exactly what the API said).
|
|
26
|
+
*/
|
|
27
|
+
function normaliseSitemap(s = {}) {
|
|
28
|
+
const contents = Array.isArray(s.contents)
|
|
29
|
+
? s.contents.map((c) => ({
|
|
30
|
+
type: c.type ?? null,
|
|
31
|
+
submitted: c.submitted ?? null,
|
|
32
|
+
indexed: c.indexed ?? null,
|
|
33
|
+
}))
|
|
34
|
+
: [];
|
|
35
|
+
return {
|
|
36
|
+
path: s.path ?? null,
|
|
37
|
+
lastSubmitted: s.lastSubmitted ?? null,
|
|
38
|
+
lastDownloaded: s.lastDownloaded ?? null,
|
|
39
|
+
isPending: s.isPending ?? null,
|
|
40
|
+
isSitemapsIndex: s.isSitemapsIndex ?? null,
|
|
41
|
+
type: s.type ?? null,
|
|
42
|
+
warnings: s.warnings ?? null,
|
|
43
|
+
errors: s.errors ?? null,
|
|
44
|
+
contents,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runSitemapStatus(args, overrides = {}) {
|
|
49
|
+
const domain = args[0];
|
|
50
|
+
const sitemapUrl = args[1];
|
|
51
|
+
|
|
52
|
+
if (!domain) {
|
|
53
|
+
process.stderr.write('Usage: pa gsc sitemap-status <domain> [sitemap-url]\n');
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
if (sitemapUrl && !/^https?:\/\//i.test(sitemapUrl)) {
|
|
57
|
+
process.stderr.write(`[gsc sitemap-status] sitemap-url must be an absolute http(s) URL, got "${sitemapUrl}"\n`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const siteUrl = toDomainProperty(domain);
|
|
62
|
+
|
|
63
|
+
let client;
|
|
64
|
+
try {
|
|
65
|
+
client = await buildGscClient(overrides);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
process.stderr.write(`[gsc sitemap-status] auth failed: ${e.message}\n`);
|
|
68
|
+
return e.exitCode || 2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let sitemaps;
|
|
72
|
+
try {
|
|
73
|
+
if (sitemapUrl) {
|
|
74
|
+
const res = await client.webmasters.sitemaps.get({ siteUrl, feedpath: sitemapUrl });
|
|
75
|
+
// sitemaps.get returns the Sitemap resource directly (no wrapper).
|
|
76
|
+
sitemaps = [normaliseSitemap(res?.data || {})];
|
|
77
|
+
} else {
|
|
78
|
+
const res = await client.webmasters.sitemaps.list({ siteUrl });
|
|
79
|
+
sitemaps = (res?.data?.sitemap || []).map(normaliseSitemap);
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
const verb = sitemapUrl ? 'sitemaps.get' : 'sitemaps.list';
|
|
83
|
+
process.stderr.write(`[gsc sitemap-status] ${verb} failed: ${e.message}\n`);
|
|
84
|
+
return 2;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const out = overrides.stdout || process.stdout;
|
|
88
|
+
out.write(JSON.stringify({
|
|
89
|
+
siteUrl,
|
|
90
|
+
sitemapUrl: sitemapUrl || null,
|
|
91
|
+
count: sitemaps.length,
|
|
92
|
+
sitemaps,
|
|
93
|
+
}, null, 2) + '\n');
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
@@ -23,7 +23,7 @@ Usage:
|
|
|
23
23
|
pa workspace <verb> Workspace server lifecycle (start | stop | status | open)
|
|
24
24
|
pa project <verb> Project state writes (create | status | doc add/rm | list)
|
|
25
25
|
pa work <verb> Work-item state writes (create | status | list)
|
|
26
|
-
pa gsc <verb> Google Search Console automation (add-property | verify-via-cf-dns | submit-sitemap | pull-analytics | list-properties)
|
|
26
|
+
pa gsc <verb> Google Search Console automation (add-property | verify-via-cf-dns | submit-sitemap | sitemap-status | pull-analytics | list-properties)
|
|
27
27
|
pa knowledge <verb> Query the operator's knowledge graph (related | search | domain | orphans | path)
|
|
28
28
|
pa research <verb> Deep-research session store (run | list | show | gates | prune | mark | paths)
|
|
29
29
|
pa autonomy <verb> Per-agent per-task-type approval state (init | get | set | list)
|
package/package.json
CHANGED