@haystackeditor/cli 0.25.1 → 0.27.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 +39 -463
- package/dist/capture/app-config.js +30 -6
- package/dist/commands/capture-brief.js +45 -35
- package/dist/commands/capture-contract.js +4 -4
- package/dist/commands/crawl-report.js +196 -0
- package/dist/commands/db-profile-upload.js +3 -3
- package/dist/commands/feedback.js +66 -0
- package/dist/commands/init-telemetry.js +55 -9
- package/dist/commands/init.js +8 -3
- package/dist/commands/lockfile-pin.js +307 -0
- package/dist/commands/telemetry-token.js +3 -3
- package/dist/commands/tokens.js +4 -4
- package/dist/commands/verify-explore.js +3 -3
- package/dist/commands/verify-history.js +2 -2
- package/dist/commands/verify-onboarding.js +13 -16
- package/dist/commands/verify-precompute.js +3 -4
- package/dist/commands/verify.js +31 -118
- package/dist/index.js +55 -968
- package/dist/schema.js +4 -10
- package/dist/utils/haystack-api.js +8 -36
- package/package.json +1 -5
- package/schemas/feedback.v1.json +13 -0
- package/schemas/pre-verify.v2.json +240 -0
- package/schemas/verify-raw.v1.json +1132 -0
- package/schemas/verify.v2.json +655 -0
- package/dist/assets/hooks/agent-context/detect.ts +0 -316
- package/dist/assets/hooks/agent-context/format.ts +0 -100
- package/dist/assets/hooks/agent-context/index.ts +0 -41
- package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
- package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
- package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
- package/dist/assets/hooks/agent-context/types.ts +0 -58
- package/dist/assets/hooks/llm-rules-template.md +0 -59
- package/dist/assets/hooks/package-lock.json +0 -598
- package/dist/assets/hooks/package.json +0 -12
- package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
- package/dist/assets/hooks/scripts/post-commit.sh +0 -5
- package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
- package/dist/assets/hooks/scripts/pre-push.sh +0 -25
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
- package/dist/assets/hooks/truncation-checker/index.ts +0 -595
- package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
- package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
- package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
- package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
- package/dist/assets/skills/map-your-system.md +0 -143
- package/dist/assets/skills/submit.md +0 -200
- package/dist/commands/ask.js +0 -20
- package/dist/commands/cloud-verifier-behaviors.js +0 -218
- package/dist/commands/cloud-verifier-data-store-census.js +0 -539
- package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
- package/dist/commands/cloud-verifier-identity-census.js +0 -4060
- package/dist/commands/cloud-verifier-materialization.js +0 -704
- package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
- package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
- package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
- package/dist/commands/cloud-verifier-universe.js +0 -10178
- package/dist/commands/config.js +0 -549
- package/dist/commands/design-verify.js +0 -311
- package/dist/commands/dismiss.js +0 -159
- package/dist/commands/hooks.js +0 -226
- package/dist/commands/inbox.js +0 -137
- package/dist/commands/mcp.js +0 -201
- package/dist/commands/policy.js +0 -371
- package/dist/commands/pr-status.js +0 -207
- package/dist/commands/pr.js +0 -105
- package/dist/commands/prepare-universe-review.js +0 -1092
- package/dist/commands/production-source-deny-policy.js +0 -100
- package/dist/commands/request-review.js +0 -74
- package/dist/commands/review.js +0 -191
- package/dist/commands/rules.js +0 -98
- package/dist/commands/scaffold-provisional-universe.js +0 -806
- package/dist/commands/setup.js +0 -1170
- package/dist/commands/skills.js +0 -447
- package/dist/commands/status.js +0 -35
- package/dist/commands/submit.js +0 -745
- package/dist/commands/system-map.js +0 -228
- package/dist/commands/triage.js +0 -598
- package/dist/commands/webhooks.js +0 -241
- package/dist/states.js +0 -46
- package/dist/tools/detect.js +0 -832
- package/dist/triage/astra.js +0 -202
- package/dist/triage/prompts.js +0 -188
- package/dist/triage/runner.js +0 -200
- package/dist/triage/types.js +0 -7
- package/dist/types.js +0 -326
- package/dist/utils/action-output.js +0 -26
- package/dist/utils/analysis-api.js +0 -416
- package/dist/utils/config.js +0 -54
- package/dist/utils/design-verifier-api.js +0 -294
- package/dist/utils/design-verifier-history.js +0 -79
- package/dist/utils/design-verifier-result.js +0 -424
- package/dist/utils/github-api.js +0 -324
- package/dist/utils/pending-state.js +0 -86
- package/dist/utils/pr-ref.js +0 -56
- package/dist/utils/prompter.js +0 -328
- package/schemas/action.v1.json +0 -22
- package/schemas/ask.v1.json +0 -40
- package/schemas/inbox.v1.json +0 -27
- package/schemas/pr-status.v1.json +0 -61
- package/schemas/pr.v1.json +0 -97
- package/schemas/pr.v3.json +0 -45
- package/schemas/setup.v1.json +0 -75
- package/schemas/submit.v1.json +0 -90
- package/schemas/triage.v1.json +0 -103
- package/schemas/triage.v2.json +0 -64
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `haystack webhooks ...` — register and inspect outbound webhooks.
|
|
3
|
-
*
|
|
4
|
-
* Two responsibilities split intentionally:
|
|
5
|
-
*
|
|
6
|
-
* Local edits to .haystack.json (`register`)
|
|
7
|
-
* The .haystack.json file IS the source of truth for which URLs a repo
|
|
8
|
-
* wants to receive events on. The wizard writes it; humans edit it;
|
|
9
|
-
* `haystack webhooks register` is a typed UI over the same edit. After
|
|
10
|
-
* writing the file, this command POSTs to the haystack-webhooks worker
|
|
11
|
-
* to create the registration row and obtain the HMAC secret.
|
|
12
|
-
*
|
|
13
|
-
* Remote queries (`list`, `deliveries`, `rotate-secret`, `replay`, etc.)
|
|
14
|
-
* Hit the worker's /admin endpoints with the user's Bearer token
|
|
15
|
-
* (introspected server-side; push access to the target repo required).
|
|
16
|
-
* Account resolution is repo-scoped so `haystack login` repoDefaults
|
|
17
|
-
* apply; id-based commands accept --repo when run outside the repo's
|
|
18
|
-
* checkout.
|
|
19
|
-
*
|
|
20
|
-
* Secret handling:
|
|
21
|
-
* The worker returns the freshly-minted HMAC secret ONCE, on register or
|
|
22
|
-
* rotate-secret. We print it to the operator and do not persist it. They
|
|
23
|
-
* are expected to stash it in their CI's secret store and reference it
|
|
24
|
-
* from .haystack.json via "secret_ref": "env:HAYSTACK_WEBHOOK_SECRET".
|
|
25
|
-
*/
|
|
26
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
27
|
-
import { resolve } from 'node:path';
|
|
28
|
-
import chalk from 'chalk';
|
|
29
|
-
import { parseRemoteUrl, UnsupportedRemoteUrlError } from '../utils/git.js';
|
|
30
|
-
import { loadToken } from '../utils/auth.js';
|
|
31
|
-
const DEFAULT_WORKER_URL = process.env.HAYSTACK_WEBHOOKS_URL ??
|
|
32
|
-
'https://haystack-webhooks.akshaysg.workers.dev';
|
|
33
|
-
// Public events accepted by the CLI. The backend may support additional
|
|
34
|
-
// gated events, but alpha-only features should not appear in normal CLI help.
|
|
35
|
-
const ALLOWED_EVENTS = [
|
|
36
|
-
'triage.completed',
|
|
37
|
-
'triage.failed',
|
|
38
|
-
'merge_queue.state_changed',
|
|
39
|
-
// 'review_chat.response_ready' deferred — chat CLI was cut, no producer.
|
|
40
|
-
'pr.classified',
|
|
41
|
-
];
|
|
42
|
-
function configPath() {
|
|
43
|
-
return resolve(process.cwd(), '.haystack.json');
|
|
44
|
-
}
|
|
45
|
-
function readConfig() {
|
|
46
|
-
const p = configPath();
|
|
47
|
-
if (!existsSync(p)) {
|
|
48
|
-
throw new Error(`.haystack.json not found in ${process.cwd()}. Run \`haystack init\` first.`);
|
|
49
|
-
}
|
|
50
|
-
return JSON.parse(readFileSync(p, 'utf8'));
|
|
51
|
-
}
|
|
52
|
-
function writeConfig(cfg) {
|
|
53
|
-
writeFileSync(configPath(), JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
54
|
-
}
|
|
55
|
-
function currentRepo() {
|
|
56
|
-
const r = parseRemoteUrl('origin');
|
|
57
|
-
return `${r.owner}/${r.repo}`;
|
|
58
|
-
}
|
|
59
|
-
/** currentRepo() for token-account resolution. Outside a git repo there is
|
|
60
|
-
* simply no repo context — expected for id-based commands run from anywhere,
|
|
61
|
-
* so that case stays quiet. A remote that EXISTS but isn't recognized is a
|
|
62
|
-
* different story: repo-scoped account defaults silently won't apply, so say
|
|
63
|
-
* so (classified by error type per PR007). */
|
|
64
|
-
export function tryCurrentRepo() {
|
|
65
|
-
try {
|
|
66
|
-
return currentRepo();
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
if (err instanceof UnsupportedRemoteUrlError) {
|
|
70
|
-
process.stderr.write(chalk.dim(` [origin remote not recognized (${err.message}); using the active account, not repo defaults]\n`));
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
/** Token-resolution scope for adminFetch: explicit repo wins, else the cwd's
|
|
76
|
-
* origin remote, else no scope (active account). Exported for tests. */
|
|
77
|
-
export function resolveTokenScope(repoFullName) {
|
|
78
|
-
const target = repoFullName ?? tryCurrentRepo();
|
|
79
|
-
if (!target || !target.includes('/'))
|
|
80
|
-
return {};
|
|
81
|
-
const [owner, repo] = target.split('/');
|
|
82
|
-
if (!owner || !repo)
|
|
83
|
-
return {};
|
|
84
|
-
return { owner, repo };
|
|
85
|
-
}
|
|
86
|
-
async function adminFetch(path, init = {}, repoFullName) {
|
|
87
|
-
// Resolve the token FOR THE TARGET REPO so `haystack login`'s repoDefaults
|
|
88
|
-
// apply, matching every other repo-scoped command (triage, submit). A bare
|
|
89
|
-
// loadToken() used the active account regardless of repo — with multiple
|
|
90
|
-
// saved accounts, register/list could 403 with "no push access" even
|
|
91
|
-
// though the repo's default account had admin (found 2026-07-06).
|
|
92
|
-
const token = await loadToken(resolveTokenScope(repoFullName));
|
|
93
|
-
const headers = {
|
|
94
|
-
'Content-Type': 'application/json',
|
|
95
|
-
...(init.headers ?? {}),
|
|
96
|
-
};
|
|
97
|
-
if (token)
|
|
98
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
99
|
-
return fetch(`${DEFAULT_WORKER_URL}${path}`, {
|
|
100
|
-
method: init.method,
|
|
101
|
-
headers,
|
|
102
|
-
body: init.body == null
|
|
103
|
-
? undefined
|
|
104
|
-
: typeof init.body === 'string'
|
|
105
|
-
? init.body
|
|
106
|
-
: JSON.stringify(init.body),
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
async function readJsonOrFail(res) {
|
|
110
|
-
const body = await res.text();
|
|
111
|
-
if (!res.ok) {
|
|
112
|
-
throw new Error(`worker ${res.status}: ${body.slice(0, 512)}`);
|
|
113
|
-
}
|
|
114
|
-
try {
|
|
115
|
-
return JSON.parse(body);
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
throw new Error(`worker returned non-JSON: ${body.slice(0, 200)}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
// ─── register ────────────────────────────────────────────────────────────────
|
|
122
|
-
export async function registerWebhook(opts) {
|
|
123
|
-
const repo = opts.repo ?? currentRepo();
|
|
124
|
-
const events = opts.events ?? ['triage.completed', 'triage.failed'];
|
|
125
|
-
for (const e of events) {
|
|
126
|
-
if (!ALLOWED_EVENTS.includes(e)) {
|
|
127
|
-
throw new Error(`Unknown event "${e}". Allowed: ${ALLOWED_EVENTS.join(', ')}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const res = await adminFetch('/admin/registrations', {
|
|
131
|
-
method: 'POST',
|
|
132
|
-
body: { repo, url: opts.url, events },
|
|
133
|
-
}, repo);
|
|
134
|
-
const data = (await readJsonOrFail(res));
|
|
135
|
-
// Update .haystack.json. Append; do not overwrite an existing entry — the
|
|
136
|
-
// operator may want multiple webhooks pointing at different endpoints.
|
|
137
|
-
const cfg = readConfig();
|
|
138
|
-
cfg.webhooks = cfg.webhooks ?? [];
|
|
139
|
-
cfg.webhooks.push({
|
|
140
|
-
url: opts.url,
|
|
141
|
-
events,
|
|
142
|
-
secret_ref: opts.secretEnv ? `env:${opts.secretEnv}` : undefined,
|
|
143
|
-
});
|
|
144
|
-
writeConfig(cfg);
|
|
145
|
-
console.log(chalk.green('✓ Webhook registered'));
|
|
146
|
-
console.log(` ${chalk.dim('repo:')} ${repo}`);
|
|
147
|
-
console.log(` ${chalk.dim('url:')} ${opts.url}`);
|
|
148
|
-
console.log(` ${chalk.dim('events:')} ${events.join(', ')}`);
|
|
149
|
-
console.log(` ${chalk.dim('id:')} ${data.id}`);
|
|
150
|
-
console.log('');
|
|
151
|
-
console.log(chalk.yellow.bold('HMAC secret (save this now — it cannot be retrieved again):'));
|
|
152
|
-
console.log('');
|
|
153
|
-
console.log(` ${chalk.bold(data.secret)}`);
|
|
154
|
-
console.log('');
|
|
155
|
-
if (opts.secretEnv) {
|
|
156
|
-
console.log(chalk.dim(`Stash it in your secret store and expose it as $${opts.secretEnv}.`));
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
console.log(chalk.dim('Tip: pass --secret-env NAME to record where the secret will live.'));
|
|
160
|
-
}
|
|
161
|
-
console.log(chalk.dim('Lost the secret? Run: haystack webhooks rotate-secret ' + data.id));
|
|
162
|
-
}
|
|
163
|
-
// ─── list ────────────────────────────────────────────────────────────────────
|
|
164
|
-
export async function listWebhooks(opts) {
|
|
165
|
-
const repo = opts.repo ?? currentRepo();
|
|
166
|
-
const res = await adminFetch(`/admin/registrations?repo=${encodeURIComponent(repo)}`, {}, repo);
|
|
167
|
-
const data = (await readJsonOrFail(res));
|
|
168
|
-
if (opts.json) {
|
|
169
|
-
console.log(JSON.stringify(data, null, 2));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (data.registrations.length === 0) {
|
|
173
|
-
console.log(chalk.dim(`No webhooks registered for ${repo}.`));
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
for (const r of data.registrations) {
|
|
177
|
-
const state = r.disabled ? chalk.red(`disabled (${r.disabled_reason ?? 'unknown'})`) : chalk.green('active');
|
|
178
|
-
console.log(`${chalk.bold(r.url)} ${state}`);
|
|
179
|
-
console.log(` ${chalk.dim('id:')} ${r.id}`);
|
|
180
|
-
console.log(` ${chalk.dim('events:')} ${r.events.join(', ')}`);
|
|
181
|
-
if (r.consecutive_5xx > 0) {
|
|
182
|
-
console.log(` ${chalk.dim('5xx:')} ${r.consecutive_5xx} consecutive`);
|
|
183
|
-
}
|
|
184
|
-
console.log('');
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
// ─── rotate-secret ───────────────────────────────────────────────────────────
|
|
188
|
-
export async function rotateWebhookSecret(id, repo) {
|
|
189
|
-
const res = await adminFetch(`/admin/registrations/${encodeURIComponent(id)}/rotate-secret`, {
|
|
190
|
-
method: 'POST',
|
|
191
|
-
}, repo);
|
|
192
|
-
const data = (await readJsonOrFail(res));
|
|
193
|
-
console.log(chalk.green('✓ Secret rotated'));
|
|
194
|
-
console.log('');
|
|
195
|
-
console.log(chalk.yellow.bold('New HMAC secret:'));
|
|
196
|
-
console.log(` ${chalk.bold(data.secret)}`);
|
|
197
|
-
console.log('');
|
|
198
|
-
console.log(chalk.red.bold('The previous secret is now invalid. Update your receiver before the next event fires.'));
|
|
199
|
-
}
|
|
200
|
-
// ─── enable / disable ────────────────────────────────────────────────────────
|
|
201
|
-
export async function setWebhookEnabled(id, enabled, repo) {
|
|
202
|
-
const action = enabled ? 'enable' : 'disable';
|
|
203
|
-
const res = await adminFetch(`/admin/registrations/${encodeURIComponent(id)}/${action}`, {
|
|
204
|
-
method: 'POST',
|
|
205
|
-
}, repo);
|
|
206
|
-
await readJsonOrFail(res);
|
|
207
|
-
console.log(chalk.green(`✓ Webhook ${id} ${action}d`));
|
|
208
|
-
}
|
|
209
|
-
// ─── deliveries / replay ─────────────────────────────────────────────────────
|
|
210
|
-
export async function listDeliveries(opts) {
|
|
211
|
-
const repo = opts.repo ?? currentRepo();
|
|
212
|
-
const limit = opts.limit ?? 20;
|
|
213
|
-
const res = await adminFetch(`/admin/deliveries?repo=${encodeURIComponent(repo)}&limit=${limit}`, {}, repo);
|
|
214
|
-
const data = (await readJsonOrFail(res));
|
|
215
|
-
if (opts.json) {
|
|
216
|
-
console.log(JSON.stringify(data, null, 2));
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
if (data.deliveries.length === 0) {
|
|
220
|
-
console.log(chalk.dim(`No deliveries for ${repo}.`));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
for (const d of data.deliveries) {
|
|
224
|
-
const color = d.status === 'delivered' ? chalk.green
|
|
225
|
-
: d.status === 'dead' ? chalk.red
|
|
226
|
-
: d.status === 'failed' ? chalk.yellow
|
|
227
|
-
: chalk.dim;
|
|
228
|
-
const code = d.last_status_code != null ? ` ${d.last_status_code}` : '';
|
|
229
|
-
console.log(`${color(d.status.padEnd(10))} ${chalk.bold(d.event.padEnd(28))} ` +
|
|
230
|
-
`attempts=${d.attempts}${code} ${chalk.dim(d.id)}`);
|
|
231
|
-
if (d.last_error)
|
|
232
|
-
console.log(` ${chalk.dim('error:')} ${d.last_error}`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
export async function replayDelivery(id, repo) {
|
|
236
|
-
const res = await adminFetch(`/admin/deliveries/${encodeURIComponent(id)}/replay`, {
|
|
237
|
-
method: 'POST',
|
|
238
|
-
}, repo);
|
|
239
|
-
await readJsonOrFail(res);
|
|
240
|
-
console.log(chalk.green(`✓ Replay queued for ${id}`));
|
|
241
|
-
}
|
package/dist/states.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Canonical machine-readable PR-state vocabulary for every CLI JSON surface.
|
|
3
|
-
*
|
|
4
|
-
* One concept, one spelling: all `--json` output uses snake_case state tokens.
|
|
5
|
-
* Before this module existed the same state appeared as `good_to_merge`
|
|
6
|
-
* (triage/pr get/inbox) and `good-to-merge` (pr-status, raw server buckets),
|
|
7
|
-
* so no single parser worked across commands. Server responses that use
|
|
8
|
-
* kebab-case are normalized at the edge via `normalizeFeedBucket`.
|
|
9
|
-
*/
|
|
10
|
-
/** Public triage verdict — emitted by `triage`, `pr get`, `submit`, `pr-status`. */
|
|
11
|
-
export const TRIAGE_VERDICTS = ['good_to_merge', 'needs_review', 'needs_input'];
|
|
12
|
-
/**
|
|
13
|
-
* Map the server's internal analysis verdict ('clean' | 'has-issues' |
|
|
14
|
-
* 'needs-review', with a rating-based fallback for pre-verdict analyses) to
|
|
15
|
-
* the public triage verdict. This mapping used to be duplicated inline in
|
|
16
|
-
* triage.ts and pr.ts.
|
|
17
|
-
*/
|
|
18
|
-
export function toPublicVerdict(analysisVerdict, haystackRating) {
|
|
19
|
-
const verdict = analysisVerdict ?? ((haystackRating ?? 0) >= 5 ? 'clean' : 'has-issues');
|
|
20
|
-
if (verdict === 'clean')
|
|
21
|
-
return 'good_to_merge';
|
|
22
|
-
if (verdict === 'needs-review')
|
|
23
|
-
return 'needs_review';
|
|
24
|
-
return 'needs_input';
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Known feed buckets in canonical snake_case form. The server may introduce
|
|
28
|
-
* new buckets; `normalizeFeedBucket` converts mechanically so unknown buckets
|
|
29
|
-
* pass through in the same convention instead of crashing or leaking kebab-case.
|
|
30
|
-
*/
|
|
31
|
-
export const FEED_BUCKETS = [
|
|
32
|
-
'analyzing',
|
|
33
|
-
'auto_fixing',
|
|
34
|
-
'good_to_merge',
|
|
35
|
-
'failed_when_merging',
|
|
36
|
-
'issues',
|
|
37
|
-
'needs_assignment',
|
|
38
|
-
'needs_shepherding',
|
|
39
|
-
'needs_fixing',
|
|
40
|
-
'awaiting_review',
|
|
41
|
-
'review_requested',
|
|
42
|
-
'snoozed',
|
|
43
|
-
];
|
|
44
|
-
export function normalizeFeedBucket(bucket) {
|
|
45
|
-
return bucket == null ? null : bucket.replace(/-/g, '_');
|
|
46
|
-
}
|