@merittdev/horus 0.17.1 → 0.19.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 +3 -1
- package/dist/index.cjs +788 -62
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3057,8 +3057,8 @@ var init_version = __esm({
|
|
|
3057
3057
|
"../../packages/core/src/version.ts"() {
|
|
3058
3058
|
"use strict";
|
|
3059
3059
|
init_cjs_shims();
|
|
3060
|
-
HORUS_VERSION = true ? "0.
|
|
3061
|
-
PINNED_SOURCE_VERSION = "2.0
|
|
3060
|
+
HORUS_VERSION = true ? "0.19.0" : "dev";
|
|
3061
|
+
PINNED_SOURCE_VERSION = "2.1.0";
|
|
3062
3062
|
}
|
|
3063
3063
|
});
|
|
3064
3064
|
|
|
@@ -10183,7 +10183,7 @@ var require_jiti = __commonJS({
|
|
|
10183
10183
|
}, pathe_M_eThtNZ_dirname = function(e3) {
|
|
10184
10184
|
const t4 = pathe_M_eThtNZ_normalizeWindowsPath(e3).replace(/\/$/, "").split("/").slice(0, -1);
|
|
10185
10185
|
return 1 === t4.length && Je3.test(t4[0]) && (t4[0] += "/"), t4.join("/") || (isAbsolute2(e3) ? "/" : ".");
|
|
10186
|
-
},
|
|
10186
|
+
}, basename6 = function(e3, t4) {
|
|
10187
10187
|
const i4 = pathe_M_eThtNZ_normalizeWindowsPath(e3).split("/");
|
|
10188
10188
|
let n5 = "";
|
|
10189
10189
|
for (let e4 = i4.length - 1; e4 >= 0; e4--) {
|
|
@@ -10904,7 +10904,7 @@ Default "index" lookups for the main are deprecated for ES modules.`, "Deprecati
|
|
|
10904
10904
|
if (!e3.opts.fsCache || !t4.filename) return i4();
|
|
10905
10905
|
const n5 = ` /* v${Ti}-${utils_hash(t4.source, 16)} */
|
|
10906
10906
|
`;
|
|
10907
|
-
let a3 = `${
|
|
10907
|
+
let a3 = `${basename6(pathe_M_eThtNZ_dirname(t4.filename))}-${(function(e4) {
|
|
10908
10908
|
const t5 = e4.split(Wt2).pop();
|
|
10909
10909
|
if (!t5) return;
|
|
10910
10910
|
const i5 = t5.lastIndexOf(".");
|
|
@@ -10950,7 +10950,7 @@ Default "index" lookups for the main are deprecated for ES modules.`, "Deprecati
|
|
|
10950
10950
|
return i4.startsWith("#!") && (i4 = "// " + i4), i4;
|
|
10951
10951
|
}
|
|
10952
10952
|
function eval_evalModule(t4, i4, n5 = {}) {
|
|
10953
|
-
const a3 = n5.id || (n5.filename ?
|
|
10953
|
+
const a3 = n5.id || (n5.filename ? basename6(n5.filename) : `_jitiEval.${n5.ext || (n5.async ? "mjs" : "js")}`), c5 = n5.filename || jitiResolve(t4, a3, { async: n5.async }), l3 = n5.ext || extname(c5), y6 = n5.cache || t4.parentCache || {}, E3 = /\.[cm]?tsx?$/.test(l3), w5 = ".mjs" === l3 || ".js" === l3 && "module" === (function(e3) {
|
|
10954
10954
|
for (; e3 && "." !== e3 && "/" !== e3; ) {
|
|
10955
10955
|
e3 = pathe_M_eThtNZ_join(e3, "..");
|
|
10956
10956
|
try {
|
|
@@ -10998,7 +10998,7 @@ Default "index" lookups for the main are deprecated for ES modules.`, "Deprecati
|
|
|
10998
10998
|
(0, $e3.mkdirSync)(n7, { recursive: true });
|
|
10999
10999
|
} catch {
|
|
11000
11000
|
}
|
|
11001
|
-
const a5 = pathe_M_eThtNZ_join(n7, `${
|
|
11001
|
+
const a5 = pathe_M_eThtNZ_join(n7, `${basename6(i8, extname(i8))}-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
|
|
11002
11002
|
return (0, $e3.writeFileSync)(a5, t6), a5;
|
|
11003
11003
|
})(l4, n6), debug(t5, "[esm]", "[tempfile]", c7), a4(pathToFileURL2(c7))), E4 = y7 ? a4(y7).catch((e3) => {
|
|
11004
11004
|
if ("ENAMETOOLONG" !== e3?.code) throw e3;
|
|
@@ -11807,6 +11807,18 @@ function resolveEnvironment(config2, opts) {
|
|
|
11807
11807
|
...url !== void 0 ? { url } : {}
|
|
11808
11808
|
};
|
|
11809
11809
|
}
|
|
11810
|
+
if (c4.shopify !== void 0) {
|
|
11811
|
+
const sh = c4.shopify;
|
|
11812
|
+
const accessId = sh.accessId ?? process.env[sh.accessIdEnv ?? "SHOPIFY_ACCESS_ID"];
|
|
11813
|
+
const secret = sh.secret ?? process.env[sh.secretEnv ?? "SHOPIFY_SECRET"];
|
|
11814
|
+
resolved.shopify = {
|
|
11815
|
+
store: sh.store,
|
|
11816
|
+
...sh.apiVersion !== void 0 ? { apiVersion: sh.apiVersion } : {},
|
|
11817
|
+
...accessId !== void 0 ? { accessId } : {},
|
|
11818
|
+
...secret !== void 0 ? { secret } : {},
|
|
11819
|
+
...sh.queries !== void 0 ? { queries: sh.queries } : {}
|
|
11820
|
+
};
|
|
11821
|
+
}
|
|
11810
11822
|
if (c4.grafana !== void 0) {
|
|
11811
11823
|
const g4 = c4.grafana;
|
|
11812
11824
|
resolved.grafana = {
|
|
@@ -12126,6 +12138,49 @@ var init_config = __esm({
|
|
|
12126
12138
|
/** Env var holding the base URL. Defaults to "AXIOM_URL". */
|
|
12127
12139
|
urlEnv: external_exports.string().optional()
|
|
12128
12140
|
}).optional(),
|
|
12141
|
+
/**
|
|
12142
|
+
* Shopify Admin connector — a first-class evidence source for the investigation
|
|
12143
|
+
* engine. The connector embeds NO queries: the GraphQL document is supplied by the
|
|
12144
|
+
* caller at investigation time (`--shopify-query`) and/or declared here as named
|
|
12145
|
+
* default queries that `horus watch` (which has no agent to pass a flag) runs each
|
|
12146
|
+
* cycle. Auth is a read-only Client-Credentials grant: `accessId` is the app
|
|
12147
|
+
* `client_id` (readable), `secret` is the `client_secret` (encrypted at rest).
|
|
12148
|
+
*/
|
|
12149
|
+
shopify: external_exports.object({
|
|
12150
|
+
/** Store subdomain, e.g. "my-store" — `.myshopify.com` is added automatically (a full domain is also accepted). */
|
|
12151
|
+
store: external_exports.string(),
|
|
12152
|
+
/** Admin API version segment, e.g. "2025-10". Defaults when omitted. */
|
|
12153
|
+
apiVersion: external_exports.string().optional(),
|
|
12154
|
+
/** App client_id (Client-Credentials grant). Takes priority over accessIdEnv. */
|
|
12155
|
+
accessId: external_exports.string().optional(),
|
|
12156
|
+
/** Env var holding the client_id. Defaults to "SHOPIFY_ACCESS_ID". */
|
|
12157
|
+
accessIdEnv: external_exports.string().optional(),
|
|
12158
|
+
/** App client_secret (encrypted at rest). Takes priority over secretEnv. */
|
|
12159
|
+
secret: external_exports.string().optional(),
|
|
12160
|
+
/** Env var holding the client_secret. Defaults to "SHOPIFY_SECRET". */
|
|
12161
|
+
secretEnv: external_exports.string().optional(),
|
|
12162
|
+
/**
|
|
12163
|
+
* Default named GraphQL queries run when no `--shopify-query` is supplied at
|
|
12164
|
+
* invocation (notably by `horus watch`). Each result set becomes Evidence. The
|
|
12165
|
+
* query text is authored by the operator, executed verbatim — never built in code.
|
|
12166
|
+
*/
|
|
12167
|
+
queries: external_exports.array(
|
|
12168
|
+
external_exports.object({
|
|
12169
|
+
/** Label for provenance + the evidence title. */
|
|
12170
|
+
name: external_exports.string(),
|
|
12171
|
+
/** Verbatim Admin GraphQL query document (read-only). */
|
|
12172
|
+
query: external_exports.string(),
|
|
12173
|
+
/** Static variables merged under any engine-bound window variables. */
|
|
12174
|
+
variables: external_exports.record(external_exports.unknown()).optional(),
|
|
12175
|
+
/** EvidenceKind to map results onto (constrained to the existing union). */
|
|
12176
|
+
kind: external_exports.enum(["state", "log", "metric"]).default("state"),
|
|
12177
|
+
/** Base relevance seed (0–1) before hint-term weighting. */
|
|
12178
|
+
relevance: external_exports.number().min(0).max(1).optional(),
|
|
12179
|
+
/** Inject the investigation window as `$from`/`$to` variables when declared. */
|
|
12180
|
+
bindWindow: external_exports.boolean().optional()
|
|
12181
|
+
})
|
|
12182
|
+
).optional()
|
|
12183
|
+
}).optional(),
|
|
12129
12184
|
grafana: external_exports.object({
|
|
12130
12185
|
dashboard: external_exports.string().optional(),
|
|
12131
12186
|
/** Multiple dashboard UIDs to fetch. Takes precedence over `dashboard` when set. */
|
|
@@ -12235,7 +12290,8 @@ var init_config = __esm({
|
|
|
12235
12290
|
sentry: ["authToken"],
|
|
12236
12291
|
axiom: ["token"],
|
|
12237
12292
|
grafana: ["username", "password"],
|
|
12238
|
-
redis: ["url"]
|
|
12293
|
+
redis: ["url"],
|
|
12294
|
+
shopify: ["secret"]
|
|
12239
12295
|
};
|
|
12240
12296
|
CONFIG_EXAMPLES = {
|
|
12241
12297
|
"(root)": 'add `database: { url: "postgresql://..." }` and at least one project',
|
|
@@ -21009,7 +21065,7 @@ var require_execute_operation = __commonJS({
|
|
|
21009
21065
|
return client.topology;
|
|
21010
21066
|
}
|
|
21011
21067
|
var BASE_BACKOFF_MS = 100;
|
|
21012
|
-
var
|
|
21068
|
+
var MAX_BACKOFF_MS2 = 1e4;
|
|
21013
21069
|
async function executeOperationWithRetries(operation, { topology, timeoutContext, session, readPreference }) {
|
|
21014
21070
|
let selector;
|
|
21015
21071
|
if (operation.hasAspect(operation_1.Aspect.MUST_SELECT_SAME_SERVER)) {
|
|
@@ -21084,7 +21140,7 @@ var require_execute_operation = __commonJS({
|
|
|
21084
21140
|
session.unpin({ force: true });
|
|
21085
21141
|
}
|
|
21086
21142
|
if (operationError.hasErrorLabel(error_1.MongoErrorLabel.SystemOverloadedError)) {
|
|
21087
|
-
const backoffMS = Math.random() * Math.min(
|
|
21143
|
+
const backoffMS = Math.random() * Math.min(MAX_BACKOFF_MS2, BASE_BACKOFF_MS * 2 ** attempt);
|
|
21088
21144
|
if (timeoutContext.csotEnabled() && backoffMS > timeoutContext.remainingTimeMS) {
|
|
21089
21145
|
throw error3;
|
|
21090
21146
|
}
|
|
@@ -72992,7 +73048,12 @@ function shortTs(iso) {
|
|
|
72992
73048
|
function buildErrorAnalysisBody(q2, field = "event_code", mapping = MERITT_FIELD_MAPPING) {
|
|
72993
73049
|
const filters = [];
|
|
72994
73050
|
if (q2.eventCode === void 0) {
|
|
72995
|
-
const
|
|
73051
|
+
const hasText = (q2.text ?? "").trim().length > 0;
|
|
73052
|
+
let minLevel = "error";
|
|
73053
|
+
if (q2.level !== void 0) {
|
|
73054
|
+
if (levelToValue(q2.level) > 50) minLevel = q2.level;
|
|
73055
|
+
else if (hasText && levelToValue(q2.level) >= 40) minLevel = q2.level;
|
|
73056
|
+
}
|
|
72996
73057
|
filters.push(buildLevelFilter(mapping, minLevel));
|
|
72997
73058
|
} else if (q2.level !== void 0 && levelToValue(q2.level) > 50) {
|
|
72998
73059
|
filters.push(buildLevelFilter(mapping, q2.level));
|
|
@@ -75142,6 +75203,301 @@ function shortTs3(iso) {
|
|
|
75142
75203
|
return `${iso.slice(5, 10)} ${iso.slice(11, 16)}`;
|
|
75143
75204
|
}
|
|
75144
75205
|
|
|
75206
|
+
// ../../packages/connectors/src/shopify/client.ts
|
|
75207
|
+
init_cjs_shims();
|
|
75208
|
+
var REQUEST_TIMEOUT_MS3 = 8e3;
|
|
75209
|
+
var DEFAULT_API_VERSION = "2025-10";
|
|
75210
|
+
var TOKEN_REFRESH_MARGIN_S = 120;
|
|
75211
|
+
var MAX_RETRIES = 2;
|
|
75212
|
+
var MAX_BACKOFF_MS = 5e3;
|
|
75213
|
+
function normalizeStore(raw) {
|
|
75214
|
+
const s5 = raw.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
|
|
75215
|
+
return s5.includes(".") ? s5 : `${s5}.myshopify.com`;
|
|
75216
|
+
}
|
|
75217
|
+
function normalizeApiVersion(raw) {
|
|
75218
|
+
const v4 = (raw ?? "").trim();
|
|
75219
|
+
return /^20\d{2}-\d{2}$/.test(v4) ? v4 : DEFAULT_API_VERSION;
|
|
75220
|
+
}
|
|
75221
|
+
function isThrottled(result) {
|
|
75222
|
+
return (result.errors ?? []).some(
|
|
75223
|
+
(e) => e.extensions?.code === "THROTTLED"
|
|
75224
|
+
);
|
|
75225
|
+
}
|
|
75226
|
+
var ShopifyAdminClient = class {
|
|
75227
|
+
store;
|
|
75228
|
+
apiVersion;
|
|
75229
|
+
baseUrl;
|
|
75230
|
+
accessId;
|
|
75231
|
+
secret;
|
|
75232
|
+
token = null;
|
|
75233
|
+
tokenExpiresAtMs = 0;
|
|
75234
|
+
constructor(opts) {
|
|
75235
|
+
this.store = normalizeStore(opts.store);
|
|
75236
|
+
this.apiVersion = normalizeApiVersion(opts.apiVersion);
|
|
75237
|
+
this.baseUrl = (opts.baseUrl ?? `https://${this.store}`).replace(/\/$/, "");
|
|
75238
|
+
this.accessId = opts.accessId;
|
|
75239
|
+
this.secret = opts.secret;
|
|
75240
|
+
}
|
|
75241
|
+
/**
|
|
75242
|
+
* Resolve a valid Admin API token. With an `accessId`, runs the Client-Credentials
|
|
75243
|
+
* grant and caches the result until (expiry - margin); without one, returns `secret`
|
|
75244
|
+
* as a direct token. Throws on a failed grant.
|
|
75245
|
+
*/
|
|
75246
|
+
async ensureToken() {
|
|
75247
|
+
if (this.accessId === void 0 || this.accessId === "") return this.secret;
|
|
75248
|
+
if (this.token !== null && Date.now() < this.tokenExpiresAtMs) return this.token;
|
|
75249
|
+
const res = await fetch(`${this.baseUrl}/admin/oauth/access_token`, {
|
|
75250
|
+
method: "POST",
|
|
75251
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
75252
|
+
body: new URLSearchParams({
|
|
75253
|
+
client_id: this.accessId,
|
|
75254
|
+
client_secret: this.secret,
|
|
75255
|
+
grant_type: "client_credentials"
|
|
75256
|
+
}),
|
|
75257
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
|
|
75258
|
+
});
|
|
75259
|
+
if (!res.ok) {
|
|
75260
|
+
const text2 = await res.text().catch(() => "");
|
|
75261
|
+
throw new Error(`Shopify token exchange -> ${res.status}: ${text2.slice(0, 200)}`);
|
|
75262
|
+
}
|
|
75263
|
+
const json2 = await res.json();
|
|
75264
|
+
if (!json2.access_token) {
|
|
75265
|
+
throw new Error("Shopify token exchange returned no access_token");
|
|
75266
|
+
}
|
|
75267
|
+
this.token = json2.access_token;
|
|
75268
|
+
const ttl = typeof json2.expires_in === "number" ? json2.expires_in : 86400;
|
|
75269
|
+
this.tokenExpiresAtMs = Date.now() + Math.max(0, ttl - TOKEN_REFRESH_MARGIN_S) * 1e3;
|
|
75270
|
+
return this.token;
|
|
75271
|
+
}
|
|
75272
|
+
/** Drop any cached token so the next request re-runs the grant (used after a 401). */
|
|
75273
|
+
invalidateToken() {
|
|
75274
|
+
this.token = null;
|
|
75275
|
+
this.tokenExpiresAtMs = 0;
|
|
75276
|
+
}
|
|
75277
|
+
/**
|
|
75278
|
+
* Run a GraphQL document verbatim against the Admin API and return the parsed result.
|
|
75279
|
+
* Retries HTTP 429 / GraphQL THROTTLED with bounded backoff; refreshes the token once
|
|
75280
|
+
* on 401. Throws on a non-retryable HTTP error.
|
|
75281
|
+
*/
|
|
75282
|
+
async graphql(query, variables) {
|
|
75283
|
+
const url = `${this.baseUrl}/admin/api/${this.apiVersion}/graphql.json`;
|
|
75284
|
+
let httpRetries = 0;
|
|
75285
|
+
let throttleRetries = 0;
|
|
75286
|
+
let refreshedOn401 = false;
|
|
75287
|
+
for (; ; ) {
|
|
75288
|
+
const token = await this.ensureToken();
|
|
75289
|
+
const res = await fetch(url, {
|
|
75290
|
+
method: "POST",
|
|
75291
|
+
headers: {
|
|
75292
|
+
"X-Shopify-Access-Token": token,
|
|
75293
|
+
"Content-Type": "application/json",
|
|
75294
|
+
Accept: "application/json"
|
|
75295
|
+
},
|
|
75296
|
+
body: JSON.stringify({ query, variables: variables ?? {} }),
|
|
75297
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
|
|
75298
|
+
});
|
|
75299
|
+
if (res.status === 401 && !refreshedOn401) {
|
|
75300
|
+
refreshedOn401 = true;
|
|
75301
|
+
this.invalidateToken();
|
|
75302
|
+
continue;
|
|
75303
|
+
}
|
|
75304
|
+
if (res.status === 429 && httpRetries < MAX_RETRIES) {
|
|
75305
|
+
httpRetries += 1;
|
|
75306
|
+
await sleep(retryAfterMs(res, httpRetries));
|
|
75307
|
+
continue;
|
|
75308
|
+
}
|
|
75309
|
+
if (!res.ok) {
|
|
75310
|
+
const text2 = await res.text().catch(() => "");
|
|
75311
|
+
throw new Error(`Shopify GraphQL -> ${res.status}: ${text2.slice(0, 200)}`);
|
|
75312
|
+
}
|
|
75313
|
+
const result = await res.json();
|
|
75314
|
+
if (isThrottled(result) && throttleRetries < MAX_RETRIES) {
|
|
75315
|
+
throttleRetries += 1;
|
|
75316
|
+
await sleep(throttleBackoffMs(result, throttleRetries));
|
|
75317
|
+
continue;
|
|
75318
|
+
}
|
|
75319
|
+
return result;
|
|
75320
|
+
}
|
|
75321
|
+
}
|
|
75322
|
+
/**
|
|
75323
|
+
* Liveness probe: run a minimal `{ shop { name } }` query — the connector's ONLY owned
|
|
75324
|
+
* query, used purely for health / connect-time validation, NEVER for evidence. It
|
|
75325
|
+
* validates both the auth (the CC grant or the static token, resolved inside `graphql`)
|
|
75326
|
+
* AND that the token can actually read — so a bad/expired static token is caught here
|
|
75327
|
+
* rather than silently reported as "reachable". Never throws.
|
|
75328
|
+
*/
|
|
75329
|
+
async health() {
|
|
75330
|
+
try {
|
|
75331
|
+
const result = await this.graphql("{ shop { name } }");
|
|
75332
|
+
if (result.errors !== void 0 && result.errors.length > 0) {
|
|
75333
|
+
return { ok: false, detail: result.errors[0].message };
|
|
75334
|
+
}
|
|
75335
|
+
return { ok: true, detail: `shopify reachable (${this.store})` };
|
|
75336
|
+
} catch (err2) {
|
|
75337
|
+
return { ok: false, detail: err2.message };
|
|
75338
|
+
}
|
|
75339
|
+
}
|
|
75340
|
+
};
|
|
75341
|
+
function retryAfterMs(res, attempt) {
|
|
75342
|
+
const header = res.headers.get("retry-after");
|
|
75343
|
+
const secs = header !== null ? Number(header) : NaN;
|
|
75344
|
+
const ms = Number.isFinite(secs) ? secs * 1e3 : 500 * 2 ** (attempt - 1);
|
|
75345
|
+
return Math.min(Math.max(0, ms), MAX_BACKOFF_MS);
|
|
75346
|
+
}
|
|
75347
|
+
function throttleBackoffMs(result, attempt) {
|
|
75348
|
+
const cost = result.extensions?.cost;
|
|
75349
|
+
const requested = cost?.requestedQueryCost ?? 0;
|
|
75350
|
+
const rate = cost?.throttleStatus?.restoreRate ?? 0;
|
|
75351
|
+
const ms = requested > 0 && rate > 0 ? requested / rate * 1e3 : 500 * 2 ** (attempt - 1);
|
|
75352
|
+
return Math.min(Math.max(0, ms), MAX_BACKOFF_MS);
|
|
75353
|
+
}
|
|
75354
|
+
function sleep(ms) {
|
|
75355
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
75356
|
+
}
|
|
75357
|
+
|
|
75358
|
+
// ../../packages/connectors/src/shopify/provider.ts
|
|
75359
|
+
init_cjs_shims();
|
|
75360
|
+
init_src();
|
|
75361
|
+
var KIND_TO_SOURCE = {
|
|
75362
|
+
state: "state",
|
|
75363
|
+
log: "logs",
|
|
75364
|
+
metric: "metrics"
|
|
75365
|
+
};
|
|
75366
|
+
var ShopifyProvider = class {
|
|
75367
|
+
constructor(client, opts) {
|
|
75368
|
+
this.client = client;
|
|
75369
|
+
this.opts = opts;
|
|
75370
|
+
}
|
|
75371
|
+
client;
|
|
75372
|
+
opts;
|
|
75373
|
+
id = "shopify";
|
|
75374
|
+
kind = "state";
|
|
75375
|
+
/**
|
|
75376
|
+
* Run each supplied (or, as a fallback, config-declared) query verbatim and return one
|
|
75377
|
+
* record per success. The investigation window is injected as `$from`/`$to` variables for
|
|
75378
|
+
* queries that opt in (`bindWindow`); `hintTerms` are NOT used to rewrite the query — only
|
|
75379
|
+
* to weight relevance later. Best-effort: a failing query is dropped (returns nothing for
|
|
75380
|
+
* that query) so one bad document never aborts the investigation. Never throws.
|
|
75381
|
+
*/
|
|
75382
|
+
async collect(opts = {}) {
|
|
75383
|
+
const specs = opts.queries !== void 0 && opts.queries.length > 0 ? opts.queries : this.opts.queries ?? [];
|
|
75384
|
+
if (specs.length === 0) return [];
|
|
75385
|
+
const to = opts.to ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
75386
|
+
const from = opts.from;
|
|
75387
|
+
const records = await Promise.all(
|
|
75388
|
+
specs.map(async (q2) => {
|
|
75389
|
+
try {
|
|
75390
|
+
const result = await this.client.graphql(q2.query, bindWindowVars(q2, from, to));
|
|
75391
|
+
const rec = { name: q2.name, kind: q2.kind, data: result.data, timestamp: to };
|
|
75392
|
+
if (q2.relevance !== void 0) rec.relevance = q2.relevance;
|
|
75393
|
+
if (result.errors !== void 0 && result.errors.length > 0) {
|
|
75394
|
+
rec.errors = result.errors.map((e) => ({ message: e.message }));
|
|
75395
|
+
}
|
|
75396
|
+
if (result.extensions?.cost !== void 0) rec.cost = result.extensions.cost;
|
|
75397
|
+
return rec;
|
|
75398
|
+
} catch {
|
|
75399
|
+
return null;
|
|
75400
|
+
}
|
|
75401
|
+
})
|
|
75402
|
+
);
|
|
75403
|
+
return records.filter((r) => r !== null);
|
|
75404
|
+
}
|
|
75405
|
+
/** Synthesize one Evidence per record. Preserves per-query provenance. */
|
|
75406
|
+
toEvidence(records, hintTerms, collectedAt) {
|
|
75407
|
+
return records.map((rec, i3) => this.recordToEvidence(rec, hintTerms, collectedAt, i3));
|
|
75408
|
+
}
|
|
75409
|
+
/**
|
|
75410
|
+
* One-shot evidence query: collect + convert. Degrades to [] on any failure so a flaky
|
|
75411
|
+
* Shopify never aborts an investigation.
|
|
75412
|
+
*/
|
|
75413
|
+
async queryEvidence(opts = {}) {
|
|
75414
|
+
try {
|
|
75415
|
+
const hintTerms = opts.hintTerms ?? [];
|
|
75416
|
+
const records = await this.collect({
|
|
75417
|
+
...opts.from !== void 0 ? { from: opts.from } : {},
|
|
75418
|
+
...opts.to !== void 0 ? { to: opts.to } : {},
|
|
75419
|
+
...opts.queries !== void 0 ? { queries: opts.queries } : {},
|
|
75420
|
+
hintTerms
|
|
75421
|
+
});
|
|
75422
|
+
return this.toEvidence(records, hintTerms, opts.collectedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
75423
|
+
} catch {
|
|
75424
|
+
return [];
|
|
75425
|
+
}
|
|
75426
|
+
}
|
|
75427
|
+
async health() {
|
|
75428
|
+
return this.client.health();
|
|
75429
|
+
}
|
|
75430
|
+
recordToEvidence(rec, hintTerms, collectedAt, index2) {
|
|
75431
|
+
const payload = {
|
|
75432
|
+
source: "shopify",
|
|
75433
|
+
shop: this.opts.store,
|
|
75434
|
+
queryName: rec.name,
|
|
75435
|
+
data: rec.data
|
|
75436
|
+
};
|
|
75437
|
+
if (rec.cost !== void 0) payload["cost"] = rec.cost;
|
|
75438
|
+
if (rec.errors !== void 0) payload["errors"] = rec.errors;
|
|
75439
|
+
const ev = {
|
|
75440
|
+
id: `ev_shopify_${index2}`,
|
|
75441
|
+
source: KIND_TO_SOURCE[rec.kind],
|
|
75442
|
+
kind: rec.kind,
|
|
75443
|
+
title: buildTitle3(rec, this.opts.store),
|
|
75444
|
+
relevance: computeRelevance3(rec, hintTerms),
|
|
75445
|
+
payload,
|
|
75446
|
+
links: {},
|
|
75447
|
+
provenance: { query: `shopify ${this.opts.store} ${rec.name}`, collectedAt }
|
|
75448
|
+
};
|
|
75449
|
+
if (rec.timestamp !== void 0) ev.timestamp = rec.timestamp;
|
|
75450
|
+
return ev;
|
|
75451
|
+
}
|
|
75452
|
+
};
|
|
75453
|
+
function bindWindowVars(q2, from, to) {
|
|
75454
|
+
const base2 = q2.variables ?? {};
|
|
75455
|
+
if (q2.bindWindow !== true) return { ...base2 };
|
|
75456
|
+
const window2 = {};
|
|
75457
|
+
if (from !== void 0) window2["from"] = from;
|
|
75458
|
+
if (to !== void 0) window2["to"] = to;
|
|
75459
|
+
return { ...base2, ...window2 };
|
|
75460
|
+
}
|
|
75461
|
+
function buildTitle3(rec, store) {
|
|
75462
|
+
const ts = rec.timestamp ? ` \xB7 ${shortTs4(rec.timestamp)}` : "";
|
|
75463
|
+
return redactSecrets(`Shopify ${store} ${rec.name}: ${summarize2(rec)}${ts}`).slice(0, 220);
|
|
75464
|
+
}
|
|
75465
|
+
function summarize2(rec) {
|
|
75466
|
+
if (rec.errors !== void 0 && rec.errors.length > 0) return `error: ${rec.errors[0].message}`;
|
|
75467
|
+
const count = countResults(rec.data);
|
|
75468
|
+
if (count !== null) return `${count} result${count === 1 ? "" : "s"}`;
|
|
75469
|
+
return "ok";
|
|
75470
|
+
}
|
|
75471
|
+
function countResults(data) {
|
|
75472
|
+
if (data === null || typeof data !== "object") return null;
|
|
75473
|
+
let total = 0;
|
|
75474
|
+
let found = false;
|
|
75475
|
+
for (const value of Object.values(data)) {
|
|
75476
|
+
if (value !== null && typeof value === "object") {
|
|
75477
|
+
const conn = value;
|
|
75478
|
+
if (Array.isArray(conn["edges"])) {
|
|
75479
|
+
total += conn["edges"].length;
|
|
75480
|
+
found = true;
|
|
75481
|
+
} else if (Array.isArray(conn["nodes"])) {
|
|
75482
|
+
total += conn["nodes"].length;
|
|
75483
|
+
found = true;
|
|
75484
|
+
}
|
|
75485
|
+
}
|
|
75486
|
+
}
|
|
75487
|
+
return found ? total : null;
|
|
75488
|
+
}
|
|
75489
|
+
function computeRelevance3(rec, hintTerms) {
|
|
75490
|
+
let score = rec.relevance ?? 0.6;
|
|
75491
|
+
const hay = JSON.stringify(rec.data ?? {}).toLowerCase();
|
|
75492
|
+
const domainTerms = hintTerms.filter((t2) => t2.length > 2);
|
|
75493
|
+
if (domainTerms.some((t2) => hay.includes(t2.toLowerCase()))) score += 0.2;
|
|
75494
|
+
return Math.min(0.95, Math.max(0.5, score));
|
|
75495
|
+
}
|
|
75496
|
+
function shortTs4(iso) {
|
|
75497
|
+
if (!iso || iso.length < 16) return iso || "\u2014";
|
|
75498
|
+
return `${iso.slice(5, 10)} ${iso.slice(11, 16)}`;
|
|
75499
|
+
}
|
|
75500
|
+
|
|
75145
75501
|
// ../../packages/connectors/src/bullmq/client.ts
|
|
75146
75502
|
init_cjs_shims();
|
|
75147
75503
|
var import_ioredis = __toESM(require_built3(), 1);
|
|
@@ -75818,6 +76174,22 @@ function axiomForEnv(renv) {
|
|
|
75818
76174
|
{ dataset: a2.dataset }
|
|
75819
76175
|
);
|
|
75820
76176
|
}
|
|
76177
|
+
function shopifyForEnv(renv) {
|
|
76178
|
+
const s5 = renv.connectors.shopify;
|
|
76179
|
+
if (!s5 || !s5.store || !s5.secret) return null;
|
|
76180
|
+
return new ShopifyProvider(
|
|
76181
|
+
new ShopifyAdminClient({
|
|
76182
|
+
store: s5.store,
|
|
76183
|
+
secret: s5.secret,
|
|
76184
|
+
...s5.accessId !== void 0 ? { accessId: s5.accessId } : {},
|
|
76185
|
+
...s5.apiVersion !== void 0 ? { apiVersion: s5.apiVersion } : {}
|
|
76186
|
+
}),
|
|
76187
|
+
{
|
|
76188
|
+
store: s5.store,
|
|
76189
|
+
...s5.queries !== void 0 ? { queries: s5.queries } : {}
|
|
76190
|
+
}
|
|
76191
|
+
);
|
|
76192
|
+
}
|
|
75821
76193
|
function codeForRepo(config2, repoName) {
|
|
75822
76194
|
const renv = resolveEnvironment(config2, { project: repoName });
|
|
75823
76195
|
const c4 = codeForEnv(renv);
|
|
@@ -76125,6 +76497,9 @@ async function redisServerStatus(renv) {
|
|
|
76125
76497
|
return { reachable, authFailed, databases };
|
|
76126
76498
|
}
|
|
76127
76499
|
|
|
76500
|
+
// ../../packages/connectors/src/shopify/index.ts
|
|
76501
|
+
init_cjs_shims();
|
|
76502
|
+
|
|
76128
76503
|
// ../../packages/cli/src/commands/update.ts
|
|
76129
76504
|
var execFileAsync = (0, import_node_util3.promisify)(import_node_child_process5.execFile);
|
|
76130
76505
|
async function ensureBackendPinned(write) {
|
|
@@ -92486,6 +92861,32 @@ async function checkEnv(renv, deps) {
|
|
|
92486
92861
|
` ${mark("pending")} ${import_picocolors6.default.bold("Axiom")} ${import_picocolors6.default.dim("not configured")}`
|
|
92487
92862
|
);
|
|
92488
92863
|
}
|
|
92864
|
+
const shopifyCfg = renv.connectors.shopify;
|
|
92865
|
+
if (shopifyCfg) {
|
|
92866
|
+
const label = shopifyCfg.store ?? "(no store)";
|
|
92867
|
+
const shopify = (deps?.shopifyFactory ?? shopifyForEnv)(renv);
|
|
92868
|
+
if (shopify) {
|
|
92869
|
+
const h3 = await shopify.health();
|
|
92870
|
+
if (!h3.ok) {
|
|
92871
|
+
console.log(
|
|
92872
|
+
` ${mark(false)} ${import_picocolors6.default.bold("Shopify")} ${import_picocolors6.default.dim(`unreachable \xB7 ${label}`)}`
|
|
92873
|
+
);
|
|
92874
|
+
allOk = false;
|
|
92875
|
+
} else {
|
|
92876
|
+
console.log(
|
|
92877
|
+
` ${mark(true)} ${import_picocolors6.default.bold("Shopify")} ${import_picocolors6.default.dim(`reachable \xB7 ${label}`)}`
|
|
92878
|
+
);
|
|
92879
|
+
}
|
|
92880
|
+
} else {
|
|
92881
|
+
console.log(
|
|
92882
|
+
` ${mark(false)} ${import_picocolors6.default.bold("Shopify")} ${import_picocolors6.default.dim(`configured (${label}) but client secret not set`)}`
|
|
92883
|
+
);
|
|
92884
|
+
}
|
|
92885
|
+
} else {
|
|
92886
|
+
console.log(
|
|
92887
|
+
` ${mark("pending")} ${import_picocolors6.default.bold("Shopify")} ${import_picocolors6.default.dim("not configured")}`
|
|
92888
|
+
);
|
|
92889
|
+
}
|
|
92489
92890
|
const redisCfg = renv.connectors.redis;
|
|
92490
92891
|
if (redisCfg?.url) {
|
|
92491
92892
|
const server = redisServerLabel(redisCfg.url);
|
|
@@ -94400,6 +94801,103 @@ function detectDataFlowCauseAcross(contexts) {
|
|
|
94400
94801
|
return null;
|
|
94401
94802
|
}
|
|
94402
94803
|
|
|
94804
|
+
// ../../packages/engine/src/hint-matched-signal.ts
|
|
94805
|
+
init_cjs_shims();
|
|
94806
|
+
var GENERIC_MATCH_STOP = /* @__PURE__ */ new Set([
|
|
94807
|
+
"product",
|
|
94808
|
+
"products",
|
|
94809
|
+
"order",
|
|
94810
|
+
"orders",
|
|
94811
|
+
"sync",
|
|
94812
|
+
"syncing",
|
|
94813
|
+
"synced",
|
|
94814
|
+
"fetch",
|
|
94815
|
+
"fetching",
|
|
94816
|
+
"api",
|
|
94817
|
+
"data",
|
|
94818
|
+
"service",
|
|
94819
|
+
"services",
|
|
94820
|
+
"update",
|
|
94821
|
+
"updating",
|
|
94822
|
+
"request",
|
|
94823
|
+
"requests",
|
|
94824
|
+
"response",
|
|
94825
|
+
"error",
|
|
94826
|
+
"errors",
|
|
94827
|
+
"get",
|
|
94828
|
+
"getting",
|
|
94829
|
+
"save",
|
|
94830
|
+
"saving",
|
|
94831
|
+
"load",
|
|
94832
|
+
"loading",
|
|
94833
|
+
"create",
|
|
94834
|
+
"creating",
|
|
94835
|
+
"delete",
|
|
94836
|
+
"failing",
|
|
94837
|
+
"failed",
|
|
94838
|
+
"failure",
|
|
94839
|
+
"broken",
|
|
94840
|
+
"value",
|
|
94841
|
+
"values",
|
|
94842
|
+
"record",
|
|
94843
|
+
"records",
|
|
94844
|
+
"found",
|
|
94845
|
+
"not",
|
|
94846
|
+
"with",
|
|
94847
|
+
"the",
|
|
94848
|
+
"and",
|
|
94849
|
+
"for",
|
|
94850
|
+
"from",
|
|
94851
|
+
"into",
|
|
94852
|
+
"this",
|
|
94853
|
+
"that",
|
|
94854
|
+
"when",
|
|
94855
|
+
"returns",
|
|
94856
|
+
"returning",
|
|
94857
|
+
"stale",
|
|
94858
|
+
"wrong",
|
|
94859
|
+
"issue",
|
|
94860
|
+
"issues",
|
|
94861
|
+
"problem",
|
|
94862
|
+
"slow",
|
|
94863
|
+
"down",
|
|
94864
|
+
"production"
|
|
94865
|
+
]);
|
|
94866
|
+
function distinctiveHintTokens(meaningfulHintTokens) {
|
|
94867
|
+
return [...new Set(meaningfulHintTokens.map((t2) => t2.toLowerCase()))].filter(
|
|
94868
|
+
(t2) => t2.length >= 4 && !GENERIC_MATCH_STOP.has(t2)
|
|
94869
|
+
);
|
|
94870
|
+
}
|
|
94871
|
+
function codeSegments(code) {
|
|
94872
|
+
return code.toLowerCase().split(/[^a-z]+/).filter((s5) => s5.length >= 4);
|
|
94873
|
+
}
|
|
94874
|
+
function selectHintMatchedSignal(signals, meaningfulHintTokens) {
|
|
94875
|
+
const distinctive = distinctiveHintTokens(meaningfulHintTokens);
|
|
94876
|
+
if (distinctive.length === 0) return null;
|
|
94877
|
+
let best = null;
|
|
94878
|
+
for (const s5 of signals) {
|
|
94879
|
+
if (!s5.key || s5.key === "(none)" || s5.count < 2) continue;
|
|
94880
|
+
const codeHit = codeSegments(s5.key).some((seg) => distinctive.includes(seg));
|
|
94881
|
+
const msg = (s5.message ?? "").toLowerCase();
|
|
94882
|
+
const msgHits = distinctive.filter((t2) => msg.includes(t2)).length;
|
|
94883
|
+
if (!codeHit && msgHits < 2) continue;
|
|
94884
|
+
const score = (codeHit ? 1e3 : 0) + msgHits * 10 + Math.min(9, Math.log10(Math.max(1, s5.count)));
|
|
94885
|
+
if (best === null || score > best.score) {
|
|
94886
|
+
best = {
|
|
94887
|
+
code: s5.key,
|
|
94888
|
+
count: s5.count,
|
|
94889
|
+
message: s5.message ?? "",
|
|
94890
|
+
via: codeHit ? "code" : "message",
|
|
94891
|
+
score
|
|
94892
|
+
};
|
|
94893
|
+
}
|
|
94894
|
+
}
|
|
94895
|
+
if (best === null) return null;
|
|
94896
|
+
const { score: _score, ...result } = best;
|
|
94897
|
+
void _score;
|
|
94898
|
+
return result;
|
|
94899
|
+
}
|
|
94900
|
+
|
|
94403
94901
|
// ../../packages/engine/src/hypotheses.ts
|
|
94404
94902
|
init_cjs_shims();
|
|
94405
94903
|
function generateHypotheses(evidence2, _correlation, ctx) {
|
|
@@ -95656,7 +96154,7 @@ function buildRuntimeSourceStatus(evidence2, connectors) {
|
|
|
95656
96154
|
const metricsConfigured = !!connectors.grafana;
|
|
95657
96155
|
const metricsFailed = metricsConfigured && !connectors.metricsCollected;
|
|
95658
96156
|
const stateCount = evidence2.filter((e) => e.source === "state").length;
|
|
95659
|
-
const stateConfigured = !!(connectors.redis || connectors.mongodb || connectors.postgres);
|
|
96157
|
+
const stateConfigured = !!(connectors.redis || connectors.mongodb || connectors.postgres || connectors.shopify);
|
|
95660
96158
|
const queueConfigured = connectors.queue ?? evidence2.some((e) => e.source === "queue");
|
|
95661
96159
|
const queueCount = evidence2.filter((e) => e.kind === "queue-state").length;
|
|
95662
96160
|
return {
|
|
@@ -98209,6 +98707,29 @@ async function investigate(input, deps) {
|
|
|
98209
98707
|
ownershipEstimate = null;
|
|
98210
98708
|
}
|
|
98211
98709
|
}
|
|
98710
|
+
let shopifyCollected = false;
|
|
98711
|
+
if (deps.shopify) {
|
|
98712
|
+
try {
|
|
98713
|
+
const from = logWindowFrom(input.logsSince ?? input.since);
|
|
98714
|
+
const to = (/* @__PURE__ */ new Date()).toISOString();
|
|
98715
|
+
const shopifySeedBase = top ? top.filePath.split("/").pop() ?? "" : "";
|
|
98716
|
+
const shopifyTerms = top ? [.../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(shopifySeedBase)])] : [...new Set(tokenize(hint))];
|
|
98717
|
+
const records = await deps.shopify.collect({
|
|
98718
|
+
from,
|
|
98719
|
+
to,
|
|
98720
|
+
hintTerms: shopifyTerms,
|
|
98721
|
+
...input.shopifyQueries !== void 0 ? { queries: input.shopifyQueries } : {}
|
|
98722
|
+
});
|
|
98723
|
+
shopifyCollected = true;
|
|
98724
|
+
const collectedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
98725
|
+
for (const ev of deps.shopify.toEvidence(records, shopifyTerms, collectedAt2)) {
|
|
98726
|
+
ev.id = globalThis.crypto.randomUUID();
|
|
98727
|
+
evidence2.push(ev);
|
|
98728
|
+
}
|
|
98729
|
+
} catch {
|
|
98730
|
+
shopifyCollected = false;
|
|
98731
|
+
}
|
|
98732
|
+
}
|
|
98212
98733
|
normalizeEvidence(evidence2, { service: input.service, environment: input.environment });
|
|
98213
98734
|
const graph = buildGraph(evidence2);
|
|
98214
98735
|
const timeline = buildTimeline(evidence2);
|
|
@@ -98558,6 +99079,54 @@ async function investigate(input, deps) {
|
|
|
98558
99079
|
}
|
|
98559
99080
|
}
|
|
98560
99081
|
}
|
|
99082
|
+
if (deps.logs && typeof deps.logs.analyzeErrors === "function" && seedEmittedJoins.length === 0 && !sourceImpactHint && meaningfulHintTokens.length > 0) {
|
|
99083
|
+
try {
|
|
99084
|
+
const warnAnalysis = await deps.logs.analyzeErrors({
|
|
99085
|
+
service: input.service,
|
|
99086
|
+
from: logWindowFrom(input.logsSince ?? input.since),
|
|
99087
|
+
level: "warn",
|
|
99088
|
+
// Plain message match (OR semantics) — NOT broadText, whose phrase-match would
|
|
99089
|
+
// require the hint words to appear contiguously (e.g. "Sale with link not found"
|
|
99090
|
+
// wouldn't match the phrase "sale links found"). selectHintMatchedSignal then
|
|
99091
|
+
// applies the precision gate on the returned signatures.
|
|
99092
|
+
text: meaningfulHintTokens.filter((t2) => t2.length >= 4).join(" ")
|
|
99093
|
+
});
|
|
99094
|
+
const picked = selectHintMatchedSignal(warnAnalysis.signatures ?? [], meaningfulHintTokens);
|
|
99095
|
+
if (picked) {
|
|
99096
|
+
const tier = seedEmittedSeverityTier(null, picked.code, picked.message);
|
|
99097
|
+
const volumeBoost = Math.min(0.1, Math.log10(Math.max(1, picked.count)) * 0.03);
|
|
99098
|
+
const msgClause = picked.message ? ` \u2014 "${picked.message.replace(/\s+/g, " ").trim().slice(0, 80)}"` : "";
|
|
99099
|
+
const ev = mkEv(
|
|
99100
|
+
"log",
|
|
99101
|
+
`event_code ${picked.code}: ${picked.count}x in Elasticsearch \u2014 matches the reported symptom`.slice(0, 220),
|
|
99102
|
+
{
|
|
99103
|
+
signature: picked.code,
|
|
99104
|
+
count: picked.count,
|
|
99105
|
+
sampleMessage: picked.message || null,
|
|
99106
|
+
relevanceClass: "direct",
|
|
99107
|
+
relevanceReason: `event_code ${picked.code} matches the reported symptom (not structurally linked to the seed)`,
|
|
99108
|
+
hintMatched: true
|
|
99109
|
+
},
|
|
99110
|
+
{},
|
|
99111
|
+
void 0,
|
|
99112
|
+
0.75
|
|
99113
|
+
);
|
|
99114
|
+
logEvIds.push(ev.id);
|
|
99115
|
+
causeInputs.push({
|
|
99116
|
+
id: "cause:hint-matched-signal",
|
|
99117
|
+
title: `Runtime signal ${picked.code} (${picked.count}x in Elasticsearch) matches the reported symptom but isn't structurally linked to ${top?.name ?? "the seed"}${msgClause} \u2014 verify`.slice(0, 220),
|
|
99118
|
+
category: "error-correlation",
|
|
99119
|
+
sourceEvidenceIds: [ev.id],
|
|
99120
|
+
// A signal whose event_code/message names the symptom is stronger evidence than a
|
|
99121
|
+
// diffuse recent commit that merely touched the seed's file (order-of-trust), so it
|
|
99122
|
+
// ranks above the broad-commit regression floor (~0.54). Still below a seed-LINKED error.
|
|
99123
|
+
baseScore: clamp015((tier === SEV_ERROR ? 0.6 : 0.56) + volumeBoost),
|
|
99124
|
+
metadata: { blastRadius, hintMatched: true, code: picked.code, count: picked.count }
|
|
99125
|
+
});
|
|
99126
|
+
}
|
|
99127
|
+
} catch {
|
|
99128
|
+
}
|
|
99129
|
+
}
|
|
98561
99130
|
if (top && seedEv && ctx && code && !sourceImpactHint) {
|
|
98562
99131
|
const scanIds = [
|
|
98563
99132
|
.../* @__PURE__ */ new Set([...seeds.slice(1, 4).map((s5) => s5.id), ...(ctx.callees ?? []).slice(0, 4).map((c4) => c4.id)])
|
|
@@ -98811,6 +99380,10 @@ async function investigate(input, deps) {
|
|
|
98811
99380
|
// collection ran (distinguishes "no matching rows" from "collection failed").
|
|
98812
99381
|
axiom: deps.axiom != null,
|
|
98813
99382
|
axiomCollected,
|
|
99383
|
+
// Shopify is configured iff a provider was built (auth present); shopifyCollected
|
|
99384
|
+
// tracks whether its query collection ran (vs "no queries supplied / failed").
|
|
99385
|
+
shopify: deps.shopify != null,
|
|
99386
|
+
shopifyCollected,
|
|
98814
99387
|
metricsCollected,
|
|
98815
99388
|
metricsFailureReason,
|
|
98816
99389
|
logsCollected,
|
|
@@ -98824,6 +99397,8 @@ async function investigate(input, deps) {
|
|
|
98824
99397
|
sentryCollected,
|
|
98825
99398
|
axiom: deps.axiom != null,
|
|
98826
99399
|
axiomCollected,
|
|
99400
|
+
shopify: deps.shopify != null,
|
|
99401
|
+
shopifyCollected,
|
|
98827
99402
|
grafana: deps.metrics != null,
|
|
98828
99403
|
queue: deps.queue != null,
|
|
98829
99404
|
metricsCollected,
|
|
@@ -117090,6 +117665,8 @@ function formatAge2(ms) {
|
|
|
117090
117665
|
// ../../packages/cli/src/commands/investigate.ts
|
|
117091
117666
|
init_cjs_shims();
|
|
117092
117667
|
var import_picocolors17 = __toESM(require_picocolors(), 1);
|
|
117668
|
+
var import_node_fs21 = require("fs");
|
|
117669
|
+
var import_node_path23 = require("path");
|
|
117093
117670
|
init_src();
|
|
117094
117671
|
|
|
117095
117672
|
// ../../packages/cli/src/lib/cloud/investigation-sync.ts
|
|
@@ -117974,6 +118551,7 @@ async function buildInvestigationContext(renv, opts) {
|
|
|
117974
118551
|
const postgres2 = postgresForEnv(renv);
|
|
117975
118552
|
const sentry = sentryForEnv(renv);
|
|
117976
118553
|
const axiom = axiomForEnv(renv);
|
|
118554
|
+
const shopify = shopifyForEnv(renv);
|
|
117977
118555
|
const queue = queueForEnv(renv);
|
|
117978
118556
|
const redisState = redisStateForEnv(renv);
|
|
117979
118557
|
const metrics = metricsForEnv(renv);
|
|
@@ -117991,6 +118569,7 @@ async function buildInvestigationContext(renv, opts) {
|
|
|
117991
118569
|
postgres: postgres2,
|
|
117992
118570
|
sentry,
|
|
117993
118571
|
axiom,
|
|
118572
|
+
shopify,
|
|
117994
118573
|
queue,
|
|
117995
118574
|
redisState,
|
|
117996
118575
|
metrics,
|
|
@@ -118023,7 +118602,10 @@ async function runOneInvestigation(input, ctx, opts = {}) {
|
|
|
118023
118602
|
...input.scope !== void 0 ? { scope: input.scope } : {},
|
|
118024
118603
|
...input.since !== void 0 ? { since: input.since } : {},
|
|
118025
118604
|
...input.logsSince !== void 0 ? { logsSince: input.logsSince } : {},
|
|
118026
|
-
...(input.service ?? ctx.service) !== void 0 ? { service: input.service ?? ctx.service } : {}
|
|
118605
|
+
...(input.service ?? ctx.service) !== void 0 ? { service: input.service ?? ctx.service } : {},
|
|
118606
|
+
// Invocation-time Shopify queries override the connector's config defaults; when
|
|
118607
|
+
// omitted the provider falls back to `connectors.shopify.queries` (e.g. under watch).
|
|
118608
|
+
...input.shopifyQueries !== void 0 ? { shopifyQueries: input.shopifyQueries } : {}
|
|
118027
118609
|
},
|
|
118028
118610
|
{
|
|
118029
118611
|
code: ctx.code,
|
|
@@ -118036,6 +118618,7 @@ async function runOneInvestigation(input, ctx, opts = {}) {
|
|
|
118036
118618
|
postgres: ctx.postgres,
|
|
118037
118619
|
sentry: ctx.sentry,
|
|
118038
118620
|
axiom: ctx.axiom,
|
|
118621
|
+
shopify: ctx.shopify,
|
|
118039
118622
|
queue: ctx.queue,
|
|
118040
118623
|
redisState: ctx.redisState,
|
|
118041
118624
|
metrics: ctx.metrics,
|
|
@@ -118047,6 +118630,7 @@ async function runOneInvestigation(input, ctx, opts = {}) {
|
|
|
118047
118630
|
postgres: !!renv.connectors.postgres?.url,
|
|
118048
118631
|
sentry: !!renv.connectors.sentry,
|
|
118049
118632
|
axiom: !!renv.connectors.axiom,
|
|
118633
|
+
shopify: !!renv.connectors.shopify,
|
|
118050
118634
|
redis: !!renv.connectors.redis?.url,
|
|
118051
118635
|
queue: !!ctx.queue
|
|
118052
118636
|
},
|
|
@@ -118086,6 +118670,46 @@ function withDeadline(p5, ms) {
|
|
|
118086
118670
|
}
|
|
118087
118671
|
|
|
118088
118672
|
// ../../packages/cli/src/commands/investigate.ts
|
|
118673
|
+
function resolveShopifyQueries(values2, variablesRaw) {
|
|
118674
|
+
if (values2 === void 0 || values2.length === 0) return [];
|
|
118675
|
+
const variables = parseShopifyVariables(variablesRaw);
|
|
118676
|
+
let stdinCache = null;
|
|
118677
|
+
return values2.map((raw, i3) => {
|
|
118678
|
+
const value = raw.trim();
|
|
118679
|
+
let query;
|
|
118680
|
+
let name2 = `q${i3 + 1}`;
|
|
118681
|
+
if (value === "-") {
|
|
118682
|
+
stdinCache ??= (0, import_node_fs21.readFileSync)(0, "utf8");
|
|
118683
|
+
query = stdinCache;
|
|
118684
|
+
name2 = "stdin";
|
|
118685
|
+
} else if (value.startsWith("@")) {
|
|
118686
|
+
const path = value.slice(1);
|
|
118687
|
+
query = (0, import_node_fs21.readFileSync)(path, "utf8");
|
|
118688
|
+
name2 = (0, import_node_path23.basename)(path).replace(/\.[^.]+$/, "") || name2;
|
|
118689
|
+
} else {
|
|
118690
|
+
query = raw;
|
|
118691
|
+
}
|
|
118692
|
+
const spec = { name: name2, query, kind: "state" };
|
|
118693
|
+
if (variables !== void 0) spec.variables = variables;
|
|
118694
|
+
if (/\$(from|to)\b/.test(query)) spec.bindWindow = true;
|
|
118695
|
+
return spec;
|
|
118696
|
+
});
|
|
118697
|
+
}
|
|
118698
|
+
function parseShopifyVariables(raw) {
|
|
118699
|
+
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
118700
|
+
const trimmed = raw.trim();
|
|
118701
|
+
const text2 = trimmed.startsWith("@") ? (0, import_node_fs21.readFileSync)(trimmed.slice(1), "utf8") : raw;
|
|
118702
|
+
let parsed;
|
|
118703
|
+
try {
|
|
118704
|
+
parsed = JSON.parse(text2);
|
|
118705
|
+
} catch {
|
|
118706
|
+
throw new Error("--shopify-variables is not valid JSON");
|
|
118707
|
+
}
|
|
118708
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
118709
|
+
throw new Error("--shopify-variables must be a JSON object");
|
|
118710
|
+
}
|
|
118711
|
+
return parsed;
|
|
118712
|
+
}
|
|
118089
118713
|
function extractEvidenceExcerpt(e) {
|
|
118090
118714
|
if (!e.payload || typeof e.payload !== "object") return void 0;
|
|
118091
118715
|
const p5 = e.payload;
|
|
@@ -118193,6 +118817,13 @@ function classifyAIFailure(firstError) {
|
|
|
118193
118817
|
async function runInvestigate(hint, opts) {
|
|
118194
118818
|
try {
|
|
118195
118819
|
const config2 = await loadConfig(opts.config, { name: opts.name });
|
|
118820
|
+
let shopifyQueries;
|
|
118821
|
+
try {
|
|
118822
|
+
shopifyQueries = resolveShopifyQueries(opts.shopifyQuery, opts.shopifyVariables);
|
|
118823
|
+
} catch (err2) {
|
|
118824
|
+
console.error(import_picocolors17.default.red(`--shopify-query: ${err2.message}`));
|
|
118825
|
+
return 1;
|
|
118826
|
+
}
|
|
118196
118827
|
const projectName = opts.project ?? opts.repo;
|
|
118197
118828
|
let renv;
|
|
118198
118829
|
try {
|
|
@@ -118229,7 +118860,8 @@ async function runInvestigate(hint, opts) {
|
|
|
118229
118860
|
hint,
|
|
118230
118861
|
...opts.scope !== void 0 ? { scope: opts.scope } : {},
|
|
118231
118862
|
...opts.since !== void 0 ? { since: opts.since } : {},
|
|
118232
|
-
...opts.logsSince !== void 0 ? { logsSince: opts.logsSince } : {}
|
|
118863
|
+
...opts.logsSince !== void 0 ? { logsSince: opts.logsSince } : {},
|
|
118864
|
+
...shopifyQueries.length > 0 ? { shopifyQueries } : {}
|
|
118233
118865
|
},
|
|
118234
118866
|
ctx,
|
|
118235
118867
|
{ timeoutMs: timeoutSec * 1e3 }
|
|
@@ -119471,8 +120103,8 @@ async function runOwner(query, opts) {
|
|
|
119471
120103
|
|
|
119472
120104
|
// ../../packages/cli/src/commands/postmortem.ts
|
|
119473
120105
|
init_cjs_shims();
|
|
119474
|
-
var
|
|
119475
|
-
var
|
|
120106
|
+
var import_node_fs22 = require("fs");
|
|
120107
|
+
var import_node_path24 = require("path");
|
|
119476
120108
|
var import_picocolors30 = __toESM(require_picocolors(), 1);
|
|
119477
120109
|
async function runPostmortem(id, opts) {
|
|
119478
120110
|
const log = opts.write ?? ((line2) => console.log(line2));
|
|
@@ -119583,14 +120215,14 @@ _AI summary unavailable: ${validationErrors?.[0] ?? "provider error"}_
|
|
|
119583
120215
|
}
|
|
119584
120216
|
}
|
|
119585
120217
|
if (opts.output) {
|
|
119586
|
-
const outputPath = (0,
|
|
119587
|
-
if ((0,
|
|
120218
|
+
const outputPath = (0, import_node_path24.resolve)(opts.output);
|
|
120219
|
+
if ((0, import_node_fs22.existsSync)(outputPath) && !opts.force) {
|
|
119588
120220
|
log(import_picocolors30.default.red("\u2717") + " " + outputPath + " already exists");
|
|
119589
120221
|
log(import_picocolors30.default.dim(" pass --force to overwrite"));
|
|
119590
120222
|
return 1;
|
|
119591
120223
|
}
|
|
119592
120224
|
try {
|
|
119593
|
-
(0,
|
|
120225
|
+
(0, import_node_fs22.writeFileSync)(outputPath, content, "utf8");
|
|
119594
120226
|
} catch (err2) {
|
|
119595
120227
|
log(import_picocolors30.default.red("\u2717 Could not write " + outputPath + ": ") + err2.message);
|
|
119596
120228
|
return 1;
|
|
@@ -121995,13 +122627,13 @@ async function runState(opts) {
|
|
|
121995
122627
|
// ../../packages/cli/src/commands/init.ts
|
|
121996
122628
|
init_cjs_shims();
|
|
121997
122629
|
var import_picocolors39 = __toESM(require_picocolors(), 1);
|
|
121998
|
-
var
|
|
122630
|
+
var import_node_path25 = require("path");
|
|
121999
122631
|
init_src();
|
|
122000
122632
|
async function runInit(opts) {
|
|
122001
122633
|
try {
|
|
122002
122634
|
const cwd = process.cwd();
|
|
122003
|
-
const root = opts.path ? (0,
|
|
122004
|
-
const name2 = opts.name ?? (0,
|
|
122635
|
+
const root = opts.path ? (0, import_node_path25.resolve)(opts.path) : findRepoRoot(cwd) ?? cwd;
|
|
122636
|
+
const name2 = opts.name ?? (0, import_node_path25.basename)(root);
|
|
122005
122637
|
const envName = opts.env ?? "production";
|
|
122006
122638
|
const repo = { name: name2, path: root };
|
|
122007
122639
|
if (opts.source) repo["source"] = { hostUrl: opts.source };
|
|
@@ -122653,7 +123285,7 @@ function parseDbSpec(spec) {
|
|
|
122653
123285
|
}
|
|
122654
123286
|
return { db, roles };
|
|
122655
123287
|
}
|
|
122656
|
-
var SUPPORTED = ["elasticsearch", "mongodb", "postgres", "sentry", "axiom", "grafana", "redis"];
|
|
123288
|
+
var SUPPORTED = ["elasticsearch", "mongodb", "postgres", "sentry", "axiom", "shopify", "grafana", "redis"];
|
|
122657
123289
|
async function runConnect(type, opts) {
|
|
122658
123290
|
if (type === "ai") {
|
|
122659
123291
|
return runConnectAi({
|
|
@@ -122693,6 +123325,9 @@ async function runConnect(type, opts) {
|
|
|
122693
123325
|
case "axiom":
|
|
122694
123326
|
patch = buildAxiomPatch(filled);
|
|
122695
123327
|
break;
|
|
123328
|
+
case "shopify":
|
|
123329
|
+
patch = buildShopifyPatch(filled);
|
|
123330
|
+
break;
|
|
122696
123331
|
case "grafana":
|
|
122697
123332
|
patch = buildGrafanaPatch(filled);
|
|
122698
123333
|
break;
|
|
@@ -122843,6 +123478,12 @@ ${import_picocolors44.default.bold(`Connect ${type}`)} ${import_picocolors44.def
|
|
|
122843
123478
|
}
|
|
122844
123479
|
break;
|
|
122845
123480
|
}
|
|
123481
|
+
case "shopify":
|
|
123482
|
+
filled.store = filled.store ?? await ask("Store name (subdomain only, e.g. 946011-c0)", "my-store");
|
|
123483
|
+
filled.apiVersion = filled.apiVersion ?? await ask("API version", "2025-10");
|
|
123484
|
+
filled.accessId = filled.accessId ?? (await ask("Access id (client_id) \u2014 leave blank for a static Admin API token", "", false) || void 0);
|
|
123485
|
+
filled.secret = filled.secret ?? (await askPassword("Secret \u2014 client_secret, or the static Admin API access token") || void 0);
|
|
123486
|
+
break;
|
|
122846
123487
|
case "grafana":
|
|
122847
123488
|
filled.url = filled.url ?? await ask("URL", "https://grafana.example.com");
|
|
122848
123489
|
filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
|
|
@@ -122902,6 +123543,8 @@ function missingRequired(type, opts) {
|
|
|
122902
123543
|
return !opts.org || !opts.project || !opts.authToken;
|
|
122903
123544
|
case "axiom":
|
|
122904
123545
|
return !opts.dataset || !opts.token;
|
|
123546
|
+
case "shopify":
|
|
123547
|
+
return !opts.store || !opts.secret;
|
|
122905
123548
|
case "grafana":
|
|
122906
123549
|
return !opts.url;
|
|
122907
123550
|
case "redis":
|
|
@@ -123007,6 +123650,14 @@ function buildAxiomPatch(opts) {
|
|
|
123007
123650
|
if (opts.url && opts.url.replace(/\/$/, "") !== "https://api.axiom.co") patch["url"] = opts.url;
|
|
123008
123651
|
return patch;
|
|
123009
123652
|
}
|
|
123653
|
+
function buildShopifyPatch(opts) {
|
|
123654
|
+
if (!opts.store) throw new Error("Store is required for shopify");
|
|
123655
|
+
const patch = { store: opts.store };
|
|
123656
|
+
if (opts.apiVersion) patch["apiVersion"] = opts.apiVersion;
|
|
123657
|
+
if (opts.accessId) patch["accessId"] = opts.accessId;
|
|
123658
|
+
if (opts.secret) patch["secret"] = opts.secret;
|
|
123659
|
+
return patch;
|
|
123660
|
+
}
|
|
123010
123661
|
function buildGrafanaPatch(opts) {
|
|
123011
123662
|
const patch = {};
|
|
123012
123663
|
if (opts.url) patch["url"] = opts.url;
|
|
@@ -123153,6 +123804,18 @@ async function probe(type, opts) {
|
|
|
123153
123804
|
});
|
|
123154
123805
|
return client.health();
|
|
123155
123806
|
}
|
|
123807
|
+
case "shopify": {
|
|
123808
|
+
if (!opts.store || !opts.secret) {
|
|
123809
|
+
return { ok: true, detail: "skipped (missing store/secret)" };
|
|
123810
|
+
}
|
|
123811
|
+
const client = new ShopifyAdminClient({
|
|
123812
|
+
store: opts.store,
|
|
123813
|
+
secret: opts.secret,
|
|
123814
|
+
...opts.accessId ? { accessId: opts.accessId } : {},
|
|
123815
|
+
...opts.apiVersion ? { apiVersion: opts.apiVersion } : {}
|
|
123816
|
+
});
|
|
123817
|
+
return client.health();
|
|
123818
|
+
}
|
|
123156
123819
|
case "grafana": {
|
|
123157
123820
|
if (!opts.url) return { ok: true, detail: "skipped (no URL)" };
|
|
123158
123821
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -123205,6 +123868,11 @@ function printSummary(type, opts) {
|
|
|
123205
123868
|
if (opts.org) lines.push(` org: ${opts.org}`);
|
|
123206
123869
|
if (opts.project) lines.push(` project: ${opts.project}`);
|
|
123207
123870
|
if (opts.dataset) lines.push(` dataset: ${opts.dataset}`);
|
|
123871
|
+
if (opts.store) lines.push(` store: ${opts.store}`);
|
|
123872
|
+
if (opts.apiVersion) lines.push(` api-version: ${opts.apiVersion}`);
|
|
123873
|
+
if (opts.accessId) lines.push(` access-id: ${opts.accessId}`);
|
|
123874
|
+
if (opts.secret)
|
|
123875
|
+
lines.push(` secret: ${"\u2022".repeat(Math.min(opts.secret.length, 8))}`);
|
|
123208
123876
|
if (opts.authToken)
|
|
123209
123877
|
lines.push(` auth-token: ${"\u2022".repeat(Math.min(opts.authToken.length, 8))}`);
|
|
123210
123878
|
if (opts.token)
|
|
@@ -123600,13 +124268,13 @@ async function runNotify(sub, opts) {
|
|
|
123600
124268
|
|
|
123601
124269
|
// ../../packages/cli/src/commands/secrets.ts
|
|
123602
124270
|
init_cjs_shims();
|
|
123603
|
-
var
|
|
124271
|
+
var import_node_fs23 = require("fs");
|
|
123604
124272
|
var import_picocolors46 = __toESM(require_picocolors(), 1);
|
|
123605
124273
|
init_src();
|
|
123606
124274
|
function locate2(cwd = process.cwd()) {
|
|
123607
124275
|
const root = findRepoRoot(cwd) ?? cwd;
|
|
123608
124276
|
const configPath = discoverLocalConfig(cwd) ?? localConfigPath(root);
|
|
123609
|
-
return { root, configPath, exists: (0,
|
|
124277
|
+
return { root, configPath, exists: (0, import_node_fs23.existsSync)(configPath) };
|
|
123610
124278
|
}
|
|
123611
124279
|
function countStoredSecrets(root) {
|
|
123612
124280
|
const stored = readLocalSecrets(root).connectors;
|
|
@@ -123744,19 +124412,19 @@ export HORUS_SECRET_KEY=${key.toString("base64")}
|
|
|
123744
124412
|
// ../../packages/cli/src/commands/stop.ts
|
|
123745
124413
|
init_cjs_shims();
|
|
123746
124414
|
var import_node_child_process10 = require("child_process");
|
|
123747
|
-
var
|
|
124415
|
+
var import_node_fs24 = require("fs");
|
|
123748
124416
|
var import_node_util6 = require("util");
|
|
123749
|
-
var
|
|
124417
|
+
var import_node_path26 = require("path");
|
|
123750
124418
|
var import_picocolors47 = __toESM(require_picocolors(), 1);
|
|
123751
124419
|
init_src();
|
|
123752
124420
|
var execFileAsync3 = (0, import_node_util6.promisify)(import_node_child_process10.execFile);
|
|
123753
|
-
var unlinkAsync = (0, import_node_util6.promisify)(
|
|
124421
|
+
var unlinkAsync = (0, import_node_util6.promisify)(import_node_fs24.unlink);
|
|
123754
124422
|
var SPAWNED_HOST_FILE2 = "spawned-host.json";
|
|
123755
124423
|
var START_TIME_TOLERANCE_S = 60;
|
|
123756
124424
|
var STOP_WAIT_MS = 5e3;
|
|
123757
124425
|
var STOP_KILL_WAIT_MS = 3e3;
|
|
123758
124426
|
var STOP_POLL_MS = 200;
|
|
123759
|
-
var
|
|
124427
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
123760
124428
|
async function runStop(opts) {
|
|
123761
124429
|
try {
|
|
123762
124430
|
if (opts.all) {
|
|
@@ -123892,11 +124560,11 @@ function isOwnHostProcess(pid, port, root, args2) {
|
|
|
123892
124560
|
if (hostArgvRegex(port).test(args2)) return true;
|
|
123893
124561
|
if (!hostArgvAnyPortRegex().test(args2)) return false;
|
|
123894
124562
|
const rec = readSourceHostPid(root);
|
|
123895
|
-
return rec !== null && rec.pid === pid && Number.isFinite(rec.port) && rec.port === port && !!rec.repoPath && (0,
|
|
124563
|
+
return rec !== null && rec.pid === pid && Number.isFinite(rec.port) && rec.port === port && !!rec.repoPath && (0, import_node_path26.resolve)(rec.repoPath) === (0, import_node_path26.resolve)(root);
|
|
123896
124564
|
}
|
|
123897
124565
|
async function cleanupSpawnedRecord(root) {
|
|
123898
124566
|
try {
|
|
123899
|
-
await unlinkAsync((0,
|
|
124567
|
+
await unlinkAsync((0, import_node_path26.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
|
|
123900
124568
|
} catch {
|
|
123901
124569
|
}
|
|
123902
124570
|
}
|
|
@@ -124000,7 +124668,7 @@ async function terminatePid(pid) {
|
|
|
124000
124668
|
}
|
|
124001
124669
|
const deadline = Date.now() + waitMs;
|
|
124002
124670
|
while (Date.now() < deadline) {
|
|
124003
|
-
await
|
|
124671
|
+
await sleep2(STOP_POLL_MS);
|
|
124004
124672
|
if (await getProcessInfo(pid) === null) return true;
|
|
124005
124673
|
}
|
|
124006
124674
|
}
|
|
@@ -124320,6 +124988,29 @@ var CONNECTOR_CHECKS = {
|
|
|
124320
124988
|
};
|
|
124321
124989
|
}
|
|
124322
124990
|
},
|
|
124991
|
+
// Shopify (Admin GraphQL — application state evidence)
|
|
124992
|
+
shopify: {
|
|
124993
|
+
label: "Shopify",
|
|
124994
|
+
absentNext: "add connectors.shopify to an environment for Shopify Admin state evidence",
|
|
124995
|
+
check: (s5, ctx) => {
|
|
124996
|
+
const secretSet = s5.secret || process.env[s5.secretEnv ?? "SHOPIFY_SECRET"];
|
|
124997
|
+
if (!secretSet) {
|
|
124998
|
+
return {
|
|
124999
|
+
label: "Shopify",
|
|
125000
|
+
status: "warn",
|
|
125001
|
+
detail: `${ctx} \u2014 client secret not set`,
|
|
125002
|
+
next: "set shopify.secret or shopify.secretEnv (default SHOPIFY_SECRET), or run horus connect shopify"
|
|
125003
|
+
};
|
|
125004
|
+
}
|
|
125005
|
+
const queryCount = s5.queries?.length ?? 0;
|
|
125006
|
+
const queryNote = queryCount > 0 ? ` (${queryCount} default quer${queryCount === 1 ? "y" : "ies"})` : " (no default queries \u2014 supply --shopify-query at investigate time)";
|
|
125007
|
+
return {
|
|
125008
|
+
label: "Shopify",
|
|
125009
|
+
status: "pass",
|
|
125010
|
+
detail: `${ctx} \u2014 ${s5.store}${queryNote} [runtime ingestion pending]`
|
|
125011
|
+
};
|
|
125012
|
+
}
|
|
125013
|
+
},
|
|
124323
125014
|
// Redis / BullMQ (queue + cache/state)
|
|
124324
125015
|
redis: {
|
|
124325
125016
|
label: "Redis",
|
|
@@ -124602,8 +125293,8 @@ async function runProvidersDoctorCommand(opts) {
|
|
|
124602
125293
|
|
|
124603
125294
|
// ../../packages/cli/src/commands/generate-config.ts
|
|
124604
125295
|
init_cjs_shims();
|
|
124605
|
-
var
|
|
124606
|
-
var
|
|
125296
|
+
var import_node_fs25 = require("fs");
|
|
125297
|
+
var import_node_path27 = require("path");
|
|
124607
125298
|
var import_picocolors51 = __toESM(require_picocolors(), 1);
|
|
124608
125299
|
init_src();
|
|
124609
125300
|
function configTemplate(name2, repoPath) {
|
|
@@ -124625,6 +125316,16 @@ function configTemplate(name2, repoPath) {
|
|
|
124625
125316
|
// elasticsearch: { indexPattern: '${name2}-prod-*' },
|
|
124626
125317
|
// mongodb: { database: '${name2}' },
|
|
124627
125318
|
// grafana: {},
|
|
125319
|
+
// shopify: {
|
|
125320
|
+
// store: '${name2}', // subdomain only \u2014 .myshopify.com is added automatically
|
|
125321
|
+
// apiVersion: '2025-10',
|
|
125322
|
+
// // accessId + secret set by \`horus connect shopify\` (secret encrypted at rest).
|
|
125323
|
+
// // Default queries run by \`horus watch\`; \`horus investigate --shopify-query\` overrides.
|
|
125324
|
+
// queries: [
|
|
125325
|
+
// { name: 'recent-orders', kind: 'state', bindWindow: true,
|
|
125326
|
+
// query: 'query($from:DateTime){ orders(first:20, query:"updated_at:>=$from"){ edges{ node{ id name displayFinancialStatus } } } }' },
|
|
125327
|
+
// ],
|
|
125328
|
+
// },
|
|
124628
125329
|
},
|
|
124629
125330
|
}],
|
|
124630
125331
|
}],
|
|
@@ -124653,17 +125354,17 @@ async function runGenerateConfig(opts) {
|
|
|
124653
125354
|
const defaults2 = localConfigPath2 != null ? projectDefaults(localConfigPath2) : null;
|
|
124654
125355
|
const hasLocalConfig = defaults2 != null;
|
|
124655
125356
|
const defaultOut = hasLocalConfig ? "horus.config.example.js" : "horus.config.js";
|
|
124656
|
-
const outPath = (0,
|
|
125357
|
+
const outPath = (0, import_node_path27.resolve)(cwd, opts.out ?? defaultOut);
|
|
124657
125358
|
const name2 = opts.name ?? defaults2?.name ?? "my-project";
|
|
124658
125359
|
const repoPath = opts.repo ?? defaults2?.repoPath ?? `/path/to/${name2}`;
|
|
124659
|
-
if ((0,
|
|
125360
|
+
if ((0, import_node_fs25.existsSync)(outPath) && !opts.force) {
|
|
124660
125361
|
log(`${import_picocolors51.default.red("\u2717")} ${outPath} already exists`);
|
|
124661
125362
|
log(import_picocolors51.default.dim(" pass --force to overwrite"));
|
|
124662
125363
|
return 1;
|
|
124663
125364
|
}
|
|
124664
125365
|
try {
|
|
124665
|
-
(0,
|
|
124666
|
-
(0,
|
|
125366
|
+
(0, import_node_fs25.mkdirSync)((0, import_node_path27.dirname)(outPath), { recursive: true });
|
|
125367
|
+
(0, import_node_fs25.writeFileSync)(outPath, configTemplate(name2, repoPath), "utf8");
|
|
124667
125368
|
} catch (err2) {
|
|
124668
125369
|
log(`${import_picocolors51.default.red("\u2717")} Could not write ${outPath}: ${err2.message}`);
|
|
124669
125370
|
return 1;
|
|
@@ -124825,6 +125526,7 @@ async function runReadiness(opts) {
|
|
|
124825
125526
|
let anyPostgres = false;
|
|
124826
125527
|
let anySentry = false;
|
|
124827
125528
|
let anyAxiom = false;
|
|
125529
|
+
let anyShopify = false;
|
|
124828
125530
|
let anyRedis = false;
|
|
124829
125531
|
for (const project of globalConfig2.projects) {
|
|
124830
125532
|
for (const repo of project.repositories) {
|
|
@@ -124838,6 +125540,7 @@ async function runReadiness(opts) {
|
|
|
124838
125540
|
if (c4.postgres) anyPostgres = true;
|
|
124839
125541
|
if (c4.sentry) anySentry = true;
|
|
124840
125542
|
if (c4.axiom) anyAxiom = true;
|
|
125543
|
+
if (c4.shopify) anyShopify = true;
|
|
124841
125544
|
if (c4.redis) anyRedis = true;
|
|
124842
125545
|
}
|
|
124843
125546
|
}
|
|
@@ -124904,6 +125607,15 @@ async function runReadiness(opts) {
|
|
|
124904
125607
|
next: "run `horus connect axiom`"
|
|
124905
125608
|
}
|
|
124906
125609
|
);
|
|
125610
|
+
checks.push(
|
|
125611
|
+
anyShopify ? { label: "Shopify", status: "pass", blocking: false, detail: "configured" } : {
|
|
125612
|
+
label: "Shopify",
|
|
125613
|
+
status: "warn",
|
|
125614
|
+
blocking: false,
|
|
125615
|
+
detail: "not configured \u2014 no Shopify Admin state evidence",
|
|
125616
|
+
next: "run `horus connect shopify`"
|
|
125617
|
+
}
|
|
125618
|
+
);
|
|
124907
125619
|
checks.push(
|
|
124908
125620
|
anyRedis ? { label: "Redis", status: "pass", blocking: false, detail: "configured" } : {
|
|
124909
125621
|
label: "Redis",
|
|
@@ -124974,8 +125686,8 @@ async function runReadiness(opts) {
|
|
|
124974
125686
|
|
|
124975
125687
|
// ../../packages/cli/src/commands/skill.ts
|
|
124976
125688
|
init_cjs_shims();
|
|
124977
|
-
var
|
|
124978
|
-
var
|
|
125689
|
+
var import_node_fs26 = require("fs");
|
|
125690
|
+
var import_node_path28 = require("path");
|
|
124979
125691
|
var import_node_os10 = require("os");
|
|
124980
125692
|
var import_promises3 = require("readline/promises");
|
|
124981
125693
|
var import_picocolors53 = __toESM(require_picocolors(), 1);
|
|
@@ -124985,22 +125697,22 @@ function isValidTarget(target) {
|
|
|
124985
125697
|
}
|
|
124986
125698
|
function getLegacyClaudeSkillPath(opts) {
|
|
124987
125699
|
const base2 = opts.global ? (0, import_node_os10.homedir)() : opts.cwd ?? process.cwd();
|
|
124988
|
-
return (0,
|
|
125700
|
+
return (0, import_node_path28.join)(base2, ".claude", "skills", "horus.md");
|
|
124989
125701
|
}
|
|
124990
125702
|
function getSkillInstallPath(target, opts) {
|
|
124991
125703
|
const base2 = opts.global ? (0, import_node_os10.homedir)() : opts.cwd ?? process.cwd();
|
|
124992
125704
|
switch (target) {
|
|
124993
125705
|
case "claude":
|
|
124994
|
-
return (0,
|
|
125706
|
+
return (0, import_node_path28.join)(base2, ".claude", "skills", "horus", "SKILL.md");
|
|
124995
125707
|
case "gemini":
|
|
124996
|
-
return opts.global ? (0,
|
|
125708
|
+
return opts.global ? (0, import_node_path28.join)(base2, ".gemini", "GEMINI.md") : (0, import_node_path28.join)(base2, "GEMINI.md");
|
|
124997
125709
|
case "cursor":
|
|
124998
|
-
return opts.global ? (0,
|
|
125710
|
+
return opts.global ? (0, import_node_path28.join)(base2, ".cursorrules") : (0, import_node_path28.join)(base2, ".cursor", "rules", "horus.mdc");
|
|
124999
125711
|
case "codex":
|
|
125000
|
-
return opts.global ? (0,
|
|
125712
|
+
return opts.global ? (0, import_node_path28.join)(base2, ".codex", "AGENTS.md") : (0, import_node_path28.join)(base2, "AGENTS.md");
|
|
125001
125713
|
case "generic":
|
|
125002
125714
|
default:
|
|
125003
|
-
return (0,
|
|
125715
|
+
return (0, import_node_path28.join)(base2, ".horus", "skills", `horus-${target}.md`);
|
|
125004
125716
|
}
|
|
125005
125717
|
}
|
|
125006
125718
|
var BASE_SKILL = `# Horus Grounded Investigation Skill
|
|
@@ -125325,7 +126037,7 @@ async function runSkillInstall(target, opts) {
|
|
|
125325
126037
|
const content = generateSkillContent(target);
|
|
125326
126038
|
if (target === "claude") {
|
|
125327
126039
|
const legacyPath = getLegacyClaudeSkillPath({ global: opts.global, cwd: opts.cwd });
|
|
125328
|
-
if ((0,
|
|
126040
|
+
if ((0, import_node_fs26.existsSync)(legacyPath)) {
|
|
125329
126041
|
if (!opts.force) {
|
|
125330
126042
|
const confirmed = await (opts.confirm ?? confirm)(
|
|
125331
126043
|
`Found old flat skill file at ${legacyPath}. Replace it with the new directory layout?`
|
|
@@ -125337,7 +126049,7 @@ async function runSkillInstall(target, opts) {
|
|
|
125337
126049
|
}
|
|
125338
126050
|
}
|
|
125339
126051
|
try {
|
|
125340
|
-
(0,
|
|
126052
|
+
(0, import_node_fs26.rmSync)(legacyPath);
|
|
125341
126053
|
log(`${import_picocolors53.default.yellow("!")} Replaced old flat skill file: ${legacyPath}`);
|
|
125342
126054
|
} catch (err2) {
|
|
125343
126055
|
log(`${import_picocolors53.default.red("\u2717")} Could not remove legacy skill file ${legacyPath}: ${err2.message}`);
|
|
@@ -125345,14 +126057,14 @@ async function runSkillInstall(target, opts) {
|
|
|
125345
126057
|
}
|
|
125346
126058
|
}
|
|
125347
126059
|
}
|
|
125348
|
-
if ((0,
|
|
126060
|
+
if ((0, import_node_fs26.existsSync)(outPath) && !opts.force) {
|
|
125349
126061
|
log(`${import_picocolors53.default.red("\u2717")} ${outPath} already exists`);
|
|
125350
126062
|
log(import_picocolors53.default.dim(" pass --force to overwrite"));
|
|
125351
126063
|
return 1;
|
|
125352
126064
|
}
|
|
125353
126065
|
try {
|
|
125354
|
-
(0,
|
|
125355
|
-
(0,
|
|
126066
|
+
(0, import_node_fs26.mkdirSync)((0, import_node_path28.dirname)(outPath), { recursive: true });
|
|
126067
|
+
(0, import_node_fs26.writeFileSync)(outPath, content, "utf8");
|
|
125356
126068
|
} catch (err2) {
|
|
125357
126069
|
log(`${import_picocolors53.default.red("\u2717")} Could not write ${outPath}: ${err2.message}`);
|
|
125358
126070
|
return 1;
|
|
@@ -125397,7 +126109,7 @@ async function runSkillPath(target, opts) {
|
|
|
125397
126109
|
// ../../packages/cli/src/commands/login.ts
|
|
125398
126110
|
init_cjs_shims();
|
|
125399
126111
|
var import_picocolors54 = __toESM(require_picocolors(), 1);
|
|
125400
|
-
var
|
|
126112
|
+
var sleep3 = (ms) => new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
125401
126113
|
async function loginWithToken(apiBaseUrl, token) {
|
|
125402
126114
|
const client = new CloudClient(apiBaseUrl, token);
|
|
125403
126115
|
try {
|
|
@@ -125452,7 +126164,7 @@ async function loginWithBrowser(apiBaseUrl) {
|
|
|
125452
126164
|
const deadline = Date.now() + session.expiresIn * 1e3;
|
|
125453
126165
|
let intervalMs = Math.max(session.interval, 1) * 1e3;
|
|
125454
126166
|
while (Date.now() < deadline) {
|
|
125455
|
-
await
|
|
126167
|
+
await sleep3(intervalMs);
|
|
125456
126168
|
let poll;
|
|
125457
126169
|
try {
|
|
125458
126170
|
poll = await client.pollCliSession(session.deviceCode);
|
|
@@ -126093,7 +126805,7 @@ function installCommandTelemetry(program2) {
|
|
|
126093
126805
|
// ../../packages/cli/src/commands/eval.ts
|
|
126094
126806
|
init_cjs_shims();
|
|
126095
126807
|
var import_promises5 = require("fs/promises");
|
|
126096
|
-
var
|
|
126808
|
+
var import_node_path29 = require("path");
|
|
126097
126809
|
var import_picocolors58 = __toESM(require_picocolors(), 1);
|
|
126098
126810
|
init_src();
|
|
126099
126811
|
function resolveProject2(config2, repo) {
|
|
@@ -126144,7 +126856,7 @@ async function runEvalBuild(opts) {
|
|
|
126144
126856
|
const project = resolveProject2(config2, opts.repo);
|
|
126145
126857
|
if (!project) return 1;
|
|
126146
126858
|
const version3 = opts.version ?? CORPUS_VERSION;
|
|
126147
|
-
const outDir = opts.out ? (0,
|
|
126859
|
+
const outDir = opts.out ? (0, import_node_path29.isAbsolute)(opts.out) ? opts.out : (0, import_node_path29.resolve)(process.cwd(), opts.out) : (0, import_node_path29.join)(process.cwd(), ".horus", "eval");
|
|
126148
126860
|
const { db, sql: sql2 } = await openDb(config2.database.url);
|
|
126149
126861
|
try {
|
|
126150
126862
|
const labels = await listOutcomeLabels(db, {
|
|
@@ -126160,8 +126872,8 @@ async function runEvalBuild(opts) {
|
|
|
126160
126872
|
const build = buildCorpus(labels, resolveReport);
|
|
126161
126873
|
const artifact = serializeCorpus(build, version3);
|
|
126162
126874
|
await (0, import_promises5.mkdir)(outDir, { recursive: true });
|
|
126163
|
-
const corpusPath = (0,
|
|
126164
|
-
const manifestPath = (0,
|
|
126875
|
+
const corpusPath = (0, import_node_path29.join)(outDir, artifact.filename);
|
|
126876
|
+
const manifestPath = (0, import_node_path29.join)(outDir, `corpus-${version3}.manifest.json`);
|
|
126165
126877
|
const manifest = { ...artifact.manifest, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), project };
|
|
126166
126878
|
await (0, import_promises5.writeFile)(corpusPath, artifact.jsonl, "utf8");
|
|
126167
126879
|
await (0, import_promises5.writeFile)(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
@@ -126262,11 +126974,11 @@ async function runEvalBaseline(opts) {
|
|
|
126262
126974
|
// ../../packages/cli/src/commands/train.ts
|
|
126263
126975
|
init_cjs_shims();
|
|
126264
126976
|
var import_promises6 = require("fs/promises");
|
|
126265
|
-
var
|
|
126977
|
+
var import_node_path30 = require("path");
|
|
126266
126978
|
var import_node_os11 = require("os");
|
|
126267
126979
|
var import_picocolors59 = __toESM(require_picocolors(), 1);
|
|
126268
126980
|
init_src();
|
|
126269
|
-
var RERANKER_MODEL_PATH = (0,
|
|
126981
|
+
var RERANKER_MODEL_PATH = (0, import_node_path30.join)((0, import_node_os11.homedir)(), ".horus", "reranker.json");
|
|
126270
126982
|
function candidatesFromReport(blob) {
|
|
126271
126983
|
if (blob === null || typeof blob !== "object") return null;
|
|
126272
126984
|
const causes = blob.suspectedCauses;
|
|
@@ -126355,7 +127067,7 @@ async function runTrain(opts) {
|
|
|
126355
127067
|
return 0;
|
|
126356
127068
|
}
|
|
126357
127069
|
try {
|
|
126358
|
-
await (0, import_promises6.mkdir)((0,
|
|
127070
|
+
await (0, import_promises6.mkdir)((0, import_node_path30.join)((0, import_node_os11.homedir)(), ".horus"), { recursive: true });
|
|
126359
127071
|
const { ok: _ok, ...model } = result;
|
|
126360
127072
|
void _ok;
|
|
126361
127073
|
await (0, import_promises6.writeFile)(RERANKER_MODEL_PATH, JSON.stringify(model, null, 2) + "\n", "utf8");
|
|
@@ -126423,8 +127135,8 @@ Examples:
|
|
|
126423
127135
|
process.exitCode = await runReadiness({ config: opts.config, ai: opts.ai, aiModel: opts.aiModel });
|
|
126424
127136
|
});
|
|
126425
127137
|
program2.command("connect <type>").description(
|
|
126426
|
-
"Add or update a connector (elasticsearch / mongodb / postgres / sentry / axiom / grafana / redis / ai) in .horus/config.json"
|
|
126427
|
-
).option("--env <name>", "target environment (default: first environment in config)").option("--provider <name>", "AI provider for `connect ai` (anthropic / claude / codex / gemini)").option("--api-key <key>", "Anthropic API key for `connect ai`").option("--model <id>", "default model for `connect ai`").option("--url <url>", "connector URL or connection string (Redis with auth: redis://:password@host:6379)").option("--username <user>", "username (elasticsearch / grafana)").option("--password <pass>", "password (elasticsearch / grafana; for Redis embed in --url)").option("--index-pattern <pattern>", "Elasticsearch index pattern (required for elasticsearch)").option("--service <name>", "service name scope for log queries").option("--database <name>", "database name (required for mongodb)").option("--collections <list>", "comma-separated collection allowlist (mongodb)").option("--schema <name>", "schema to introspect (postgres; default public)").option("--tables <list>", "comma-separated table allowlist (postgres)").option("--auth-token <token>", "Sentry API auth token (required for sentry)").option("--org <slug>", "Sentry org slug (required for sentry)").option("--project <slug>", "Sentry project slug (required for sentry)").option("--token <token>", "Axiom API token (required for axiom)").option("--dataset <name>", "Axiom dataset to query (required for axiom)").option("--dashboard <uid>", "default dashboard uid (grafana)").option(
|
|
127138
|
+
"Add or update a connector (elasticsearch / mongodb / postgres / sentry / axiom / shopify / grafana / redis / ai) in .horus/config.json"
|
|
127139
|
+
).option("--env <name>", "target environment (default: first environment in config)").option("--provider <name>", "AI provider for `connect ai` (anthropic / claude / codex / gemini)").option("--api-key <key>", "Anthropic API key for `connect ai`").option("--model <id>", "default model for `connect ai`").option("--url <url>", "connector URL or connection string (Redis with auth: redis://:password@host:6379)").option("--username <user>", "username (elasticsearch / grafana)").option("--password <pass>", "password (elasticsearch / grafana; for Redis embed in --url)").option("--index-pattern <pattern>", "Elasticsearch index pattern (required for elasticsearch)").option("--service <name>", "service name scope for log queries").option("--database <name>", "database name (required for mongodb)").option("--collections <list>", "comma-separated collection allowlist (mongodb)").option("--schema <name>", "schema to introspect (postgres; default public)").option("--tables <list>", "comma-separated table allowlist (postgres)").option("--auth-token <token>", "Sentry API auth token (required for sentry)").option("--org <slug>", "Sentry org slug (required for sentry)").option("--project <slug>", "Sentry project slug (required for sentry)").option("--token <token>", "Axiom API token (required for axiom)").option("--dataset <name>", "Axiom dataset to query (required for axiom)").option("--store <name>", "Shopify store subdomain, e.g. my-store \u2014 .myshopify.com is added automatically (required for shopify)").option("--api-version <ver>", "Shopify Admin API version, e.g. 2025-10 (shopify; defaults to a recent version)").option("--access-id <id>", "Shopify app client_id \u2014 omit for a static Admin API token (shopify)").option("--secret <secret>", "Shopify client_secret, or a static Admin API access token \u2014 encrypted at rest (required for shopify)").option("--dashboard <uid>", "default dashboard uid (grafana)").option(
|
|
126428
127140
|
"--db <spec>",
|
|
126429
127141
|
"redis logical DB as db:role1,role2 (e.g. 0:cache,state or 1:bullmq,queues); repeatable",
|
|
126430
127142
|
(val, acc) => {
|
|
@@ -126450,6 +127162,10 @@ Examples:
|
|
|
126450
127162
|
project: opts.project,
|
|
126451
127163
|
token: opts.token,
|
|
126452
127164
|
dataset: opts.dataset,
|
|
127165
|
+
store: opts.store,
|
|
127166
|
+
apiVersion: opts.apiVersion,
|
|
127167
|
+
accessId: opts.accessId,
|
|
127168
|
+
secret: opts.secret,
|
|
126453
127169
|
dashboard: opts.dashboard,
|
|
126454
127170
|
db: opts.db,
|
|
126455
127171
|
bullmqPrefix: opts.bullmqPrefix,
|
|
@@ -126629,7 +127345,15 @@ score, a confidence, or a verdict. It ships OFF; once trained, enable with:
|
|
|
126629
127345
|
});
|
|
126630
127346
|
}
|
|
126631
127347
|
);
|
|
126632
|
-
program2.command("investigate <hint>").description("Run a deterministic investigation for an incident hint").option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name to scope to").option("--env <name>", "environment name (e.g. production)").option("--repo <name>", "repository/project to scope to (alias for --project)").option("--scope <path>", "resolve the seed only from symbols under this path (e.g. packages/core) \u2014 useful in monorepos").option("--since <ref>", "git ref/range for change-impact (e.g. HEAD~5)").option("--logs-since <dur>", "runtime-log window as a duration (e.g. 30d, 24h); independent of --since").option(
|
|
127348
|
+
program2.command("investigate <hint>").description("Run a deterministic investigation for an incident hint").option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name to scope to").option("--env <name>", "environment name (e.g. production)").option("--repo <name>", "repository/project to scope to (alias for --project)").option("--scope <path>", "resolve the seed only from symbols under this path (e.g. packages/core) \u2014 useful in monorepos").option("--since <ref>", "git ref/range for change-impact (e.g. HEAD~5)").option("--logs-since <dur>", "runtime-log window as a duration (e.g. 30d, 24h); independent of --since").option(
|
|
127349
|
+
"--shopify-query <q>",
|
|
127350
|
+
"Shopify Admin GraphQL query to run as evidence: @file, - (stdin), or a raw string; repeatable",
|
|
127351
|
+
(val, acc) => {
|
|
127352
|
+
acc.push(val);
|
|
127353
|
+
return acc;
|
|
127354
|
+
},
|
|
127355
|
+
[]
|
|
127356
|
+
).option("--shopify-variables <json>", "variables (@file or inline JSON object) applied to every --shopify-query").option("--timeout <sec>", "overall investigation deadline in seconds (default 120)").option(
|
|
126633
127357
|
"--service <name>",
|
|
126634
127358
|
"service name to scope runtime logs, e.g. leadcall-api-prod"
|
|
126635
127359
|
).option("--json", "output JSON (alias for --format json)").option("--format <fmt>", "output format: text | markdown | json", "text").option("--ai", "enrich report with AI narrative (requires ANTHROPIC_API_KEY; falls back to deterministic on failure)").option("--ai-model <model>", "AI model for --ai (default: claude-opus-4-8)").action(
|
|
@@ -126643,6 +127367,8 @@ score, a confidence, or a verdict. It ships OFF; once trained, enable with:
|
|
|
126643
127367
|
scope: opts.scope,
|
|
126644
127368
|
since: opts.since,
|
|
126645
127369
|
logsSince: opts.logsSince,
|
|
127370
|
+
...opts.shopifyQuery !== void 0 ? { shopifyQuery: opts.shopifyQuery } : {},
|
|
127371
|
+
...opts.shopifyVariables !== void 0 ? { shopifyVariables: opts.shopifyVariables } : {},
|
|
126646
127372
|
timeout: opts.timeout,
|
|
126647
127373
|
service: opts.service,
|
|
126648
127374
|
json: opts.json,
|