@merittdev/horus 0.18.0 → 0.19.1
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 +682 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**Understand what happened.**
|
|
8
8
|
|
|
9
|
-
Open-source incident investigation. Horus connects Elasticsearch, Sentry, Grafana, MongoDB, Postgres, Redis (incl. BullMQ queues), Axiom logs, and source intelligence into deterministic reports — installable today.
|
|
9
|
+
Open-source incident investigation. Horus connects Elasticsearch, Sentry, Grafana, MongoDB, Postgres, Redis (incl. BullMQ queues), Axiom logs, Shopify Admin, and source intelligence into deterministic reports — installable today.
|
|
10
10
|
|
|
11
11
|
CLI-only. Read-only against production systems. Horus never writes to your infrastructure.
|
|
12
12
|
|
|
@@ -129,6 +129,7 @@ Installable today. More connectors and AI providers are in progress.
|
|
|
129
129
|
- Grafana metrics
|
|
130
130
|
- MongoDB / Postgres / Redis state
|
|
131
131
|
- BullMQ queue evidence
|
|
132
|
+
- Shopify Admin (GraphQL — queries you supply)
|
|
132
133
|
- Source intelligence (code graph)
|
|
133
134
|
- Timeline generation
|
|
134
135
|
- Evidence correlation
|
|
@@ -160,6 +161,7 @@ Horus is organized in four layers:
|
|
|
160
161
|
- **MongoDB / Postgres** — application/operational state
|
|
161
162
|
- **Grafana** — metrics via its datasource proxy
|
|
162
163
|
- **Redis / BullMQ** — cache & queue runtime state
|
|
164
|
+
- **Shopify Admin** — application state via GraphQL queries you supply at investigation time (`--shopify-query`) or declare in config; the connector embeds none. Read-only, Client-Credentials auth.
|
|
163
165
|
- **Git** — change history, ownership signals
|
|
164
166
|
|
|
165
167
|
**Investigation** (deterministic)
|
package/dist/index.cjs
CHANGED
|
@@ -3057,7 +3057,7 @@ 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.
|
|
3060
|
+
HORUS_VERSION = true ? "0.19.1" : "dev";
|
|
3061
3061
|
PINNED_SOURCE_VERSION = "2.1.0";
|
|
3062
3062
|
}
|
|
3063
3063
|
});
|
|
@@ -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
|
}
|
|
@@ -75147,6 +75203,301 @@ function shortTs3(iso) {
|
|
|
75147
75203
|
return `${iso.slice(5, 10)} ${iso.slice(11, 16)}`;
|
|
75148
75204
|
}
|
|
75149
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
|
+
|
|
75150
75501
|
// ../../packages/connectors/src/bullmq/client.ts
|
|
75151
75502
|
init_cjs_shims();
|
|
75152
75503
|
var import_ioredis = __toESM(require_built3(), 1);
|
|
@@ -75823,6 +76174,22 @@ function axiomForEnv(renv) {
|
|
|
75823
76174
|
{ dataset: a2.dataset }
|
|
75824
76175
|
);
|
|
75825
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
|
+
}
|
|
75826
76193
|
function codeForRepo(config2, repoName) {
|
|
75827
76194
|
const renv = resolveEnvironment(config2, { project: repoName });
|
|
75828
76195
|
const c4 = codeForEnv(renv);
|
|
@@ -76130,6 +76497,9 @@ async function redisServerStatus(renv) {
|
|
|
76130
76497
|
return { reachable, authFailed, databases };
|
|
76131
76498
|
}
|
|
76132
76499
|
|
|
76500
|
+
// ../../packages/connectors/src/shopify/index.ts
|
|
76501
|
+
init_cjs_shims();
|
|
76502
|
+
|
|
76133
76503
|
// ../../packages/cli/src/commands/update.ts
|
|
76134
76504
|
var execFileAsync = (0, import_node_util3.promisify)(import_node_child_process5.execFile);
|
|
76135
76505
|
async function ensureBackendPinned(write) {
|
|
@@ -92491,6 +92861,32 @@ async function checkEnv(renv, deps) {
|
|
|
92491
92861
|
` ${mark("pending")} ${import_picocolors6.default.bold("Axiom")} ${import_picocolors6.default.dim("not configured")}`
|
|
92492
92862
|
);
|
|
92493
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
|
+
}
|
|
92494
92890
|
const redisCfg = renv.connectors.redis;
|
|
92495
92891
|
if (redisCfg?.url) {
|
|
92496
92892
|
const server = redisServerLabel(redisCfg.url);
|
|
@@ -95758,7 +96154,7 @@ function buildRuntimeSourceStatus(evidence2, connectors) {
|
|
|
95758
96154
|
const metricsConfigured = !!connectors.grafana;
|
|
95759
96155
|
const metricsFailed = metricsConfigured && !connectors.metricsCollected;
|
|
95760
96156
|
const stateCount = evidence2.filter((e) => e.source === "state").length;
|
|
95761
|
-
const stateConfigured = !!(connectors.redis || connectors.mongodb || connectors.postgres);
|
|
96157
|
+
const stateConfigured = !!(connectors.redis || connectors.mongodb || connectors.postgres || connectors.shopify);
|
|
95762
96158
|
const queueConfigured = connectors.queue ?? evidence2.some((e) => e.source === "queue");
|
|
95763
96159
|
const queueCount = evidence2.filter((e) => e.kind === "queue-state").length;
|
|
95764
96160
|
return {
|
|
@@ -98311,6 +98707,29 @@ async function investigate(input, deps) {
|
|
|
98311
98707
|
ownershipEstimate = null;
|
|
98312
98708
|
}
|
|
98313
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
|
+
}
|
|
98314
98733
|
normalizeEvidence(evidence2, { service: input.service, environment: input.environment });
|
|
98315
98734
|
const graph = buildGraph(evidence2);
|
|
98316
98735
|
const timeline = buildTimeline(evidence2);
|
|
@@ -98961,6 +99380,10 @@ async function investigate(input, deps) {
|
|
|
98961
99380
|
// collection ran (distinguishes "no matching rows" from "collection failed").
|
|
98962
99381
|
axiom: deps.axiom != null,
|
|
98963
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,
|
|
98964
99387
|
metricsCollected,
|
|
98965
99388
|
metricsFailureReason,
|
|
98966
99389
|
logsCollected,
|
|
@@ -98974,6 +99397,8 @@ async function investigate(input, deps) {
|
|
|
98974
99397
|
sentryCollected,
|
|
98975
99398
|
axiom: deps.axiom != null,
|
|
98976
99399
|
axiomCollected,
|
|
99400
|
+
shopify: deps.shopify != null,
|
|
99401
|
+
shopifyCollected,
|
|
98977
99402
|
grafana: deps.metrics != null,
|
|
98978
99403
|
queue: deps.queue != null,
|
|
98979
99404
|
metricsCollected,
|
|
@@ -117240,6 +117665,8 @@ function formatAge2(ms) {
|
|
|
117240
117665
|
// ../../packages/cli/src/commands/investigate.ts
|
|
117241
117666
|
init_cjs_shims();
|
|
117242
117667
|
var import_picocolors17 = __toESM(require_picocolors(), 1);
|
|
117668
|
+
var import_node_fs21 = require("fs");
|
|
117669
|
+
var import_node_path23 = require("path");
|
|
117243
117670
|
init_src();
|
|
117244
117671
|
|
|
117245
117672
|
// ../../packages/cli/src/lib/cloud/investigation-sync.ts
|
|
@@ -118124,6 +118551,7 @@ async function buildInvestigationContext(renv, opts) {
|
|
|
118124
118551
|
const postgres2 = postgresForEnv(renv);
|
|
118125
118552
|
const sentry = sentryForEnv(renv);
|
|
118126
118553
|
const axiom = axiomForEnv(renv);
|
|
118554
|
+
const shopify = shopifyForEnv(renv);
|
|
118127
118555
|
const queue = queueForEnv(renv);
|
|
118128
118556
|
const redisState = redisStateForEnv(renv);
|
|
118129
118557
|
const metrics = metricsForEnv(renv);
|
|
@@ -118141,6 +118569,7 @@ async function buildInvestigationContext(renv, opts) {
|
|
|
118141
118569
|
postgres: postgres2,
|
|
118142
118570
|
sentry,
|
|
118143
118571
|
axiom,
|
|
118572
|
+
shopify,
|
|
118144
118573
|
queue,
|
|
118145
118574
|
redisState,
|
|
118146
118575
|
metrics,
|
|
@@ -118173,7 +118602,10 @@ async function runOneInvestigation(input, ctx, opts = {}) {
|
|
|
118173
118602
|
...input.scope !== void 0 ? { scope: input.scope } : {},
|
|
118174
118603
|
...input.since !== void 0 ? { since: input.since } : {},
|
|
118175
118604
|
...input.logsSince !== void 0 ? { logsSince: input.logsSince } : {},
|
|
118176
|
-
...(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 } : {}
|
|
118177
118609
|
},
|
|
118178
118610
|
{
|
|
118179
118611
|
code: ctx.code,
|
|
@@ -118186,6 +118618,7 @@ async function runOneInvestigation(input, ctx, opts = {}) {
|
|
|
118186
118618
|
postgres: ctx.postgres,
|
|
118187
118619
|
sentry: ctx.sentry,
|
|
118188
118620
|
axiom: ctx.axiom,
|
|
118621
|
+
shopify: ctx.shopify,
|
|
118189
118622
|
queue: ctx.queue,
|
|
118190
118623
|
redisState: ctx.redisState,
|
|
118191
118624
|
metrics: ctx.metrics,
|
|
@@ -118197,6 +118630,7 @@ async function runOneInvestigation(input, ctx, opts = {}) {
|
|
|
118197
118630
|
postgres: !!renv.connectors.postgres?.url,
|
|
118198
118631
|
sentry: !!renv.connectors.sentry,
|
|
118199
118632
|
axiom: !!renv.connectors.axiom,
|
|
118633
|
+
shopify: !!renv.connectors.shopify,
|
|
118200
118634
|
redis: !!renv.connectors.redis?.url,
|
|
118201
118635
|
queue: !!ctx.queue
|
|
118202
118636
|
},
|
|
@@ -118236,6 +118670,46 @@ function withDeadline(p5, ms) {
|
|
|
118236
118670
|
}
|
|
118237
118671
|
|
|
118238
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
|
+
}
|
|
118239
118713
|
function extractEvidenceExcerpt(e) {
|
|
118240
118714
|
if (!e.payload || typeof e.payload !== "object") return void 0;
|
|
118241
118715
|
const p5 = e.payload;
|
|
@@ -118343,6 +118817,13 @@ function classifyAIFailure(firstError) {
|
|
|
118343
118817
|
async function runInvestigate(hint, opts) {
|
|
118344
118818
|
try {
|
|
118345
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
|
+
}
|
|
118346
118827
|
const projectName = opts.project ?? opts.repo;
|
|
118347
118828
|
let renv;
|
|
118348
118829
|
try {
|
|
@@ -118379,7 +118860,8 @@ async function runInvestigate(hint, opts) {
|
|
|
118379
118860
|
hint,
|
|
118380
118861
|
...opts.scope !== void 0 ? { scope: opts.scope } : {},
|
|
118381
118862
|
...opts.since !== void 0 ? { since: opts.since } : {},
|
|
118382
|
-
...opts.logsSince !== void 0 ? { logsSince: opts.logsSince } : {}
|
|
118863
|
+
...opts.logsSince !== void 0 ? { logsSince: opts.logsSince } : {},
|
|
118864
|
+
...shopifyQueries.length > 0 ? { shopifyQueries } : {}
|
|
118383
118865
|
},
|
|
118384
118866
|
ctx,
|
|
118385
118867
|
{ timeoutMs: timeoutSec * 1e3 }
|
|
@@ -119621,8 +120103,8 @@ async function runOwner(query, opts) {
|
|
|
119621
120103
|
|
|
119622
120104
|
// ../../packages/cli/src/commands/postmortem.ts
|
|
119623
120105
|
init_cjs_shims();
|
|
119624
|
-
var
|
|
119625
|
-
var
|
|
120106
|
+
var import_node_fs22 = require("fs");
|
|
120107
|
+
var import_node_path24 = require("path");
|
|
119626
120108
|
var import_picocolors30 = __toESM(require_picocolors(), 1);
|
|
119627
120109
|
async function runPostmortem(id, opts) {
|
|
119628
120110
|
const log = opts.write ?? ((line2) => console.log(line2));
|
|
@@ -119733,14 +120215,14 @@ _AI summary unavailable: ${validationErrors?.[0] ?? "provider error"}_
|
|
|
119733
120215
|
}
|
|
119734
120216
|
}
|
|
119735
120217
|
if (opts.output) {
|
|
119736
|
-
const outputPath = (0,
|
|
119737
|
-
if ((0,
|
|
120218
|
+
const outputPath = (0, import_node_path24.resolve)(opts.output);
|
|
120219
|
+
if ((0, import_node_fs22.existsSync)(outputPath) && !opts.force) {
|
|
119738
120220
|
log(import_picocolors30.default.red("\u2717") + " " + outputPath + " already exists");
|
|
119739
120221
|
log(import_picocolors30.default.dim(" pass --force to overwrite"));
|
|
119740
120222
|
return 1;
|
|
119741
120223
|
}
|
|
119742
120224
|
try {
|
|
119743
|
-
(0,
|
|
120225
|
+
(0, import_node_fs22.writeFileSync)(outputPath, content, "utf8");
|
|
119744
120226
|
} catch (err2) {
|
|
119745
120227
|
log(import_picocolors30.default.red("\u2717 Could not write " + outputPath + ": ") + err2.message);
|
|
119746
120228
|
return 1;
|
|
@@ -122145,13 +122627,13 @@ async function runState(opts) {
|
|
|
122145
122627
|
// ../../packages/cli/src/commands/init.ts
|
|
122146
122628
|
init_cjs_shims();
|
|
122147
122629
|
var import_picocolors39 = __toESM(require_picocolors(), 1);
|
|
122148
|
-
var
|
|
122630
|
+
var import_node_path25 = require("path");
|
|
122149
122631
|
init_src();
|
|
122150
122632
|
async function runInit(opts) {
|
|
122151
122633
|
try {
|
|
122152
122634
|
const cwd = process.cwd();
|
|
122153
|
-
const root = opts.path ? (0,
|
|
122154
|
-
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);
|
|
122155
122637
|
const envName = opts.env ?? "production";
|
|
122156
122638
|
const repo = { name: name2, path: root };
|
|
122157
122639
|
if (opts.source) repo["source"] = { hostUrl: opts.source };
|
|
@@ -122803,7 +123285,7 @@ function parseDbSpec(spec) {
|
|
|
122803
123285
|
}
|
|
122804
123286
|
return { db, roles };
|
|
122805
123287
|
}
|
|
122806
|
-
var SUPPORTED = ["elasticsearch", "mongodb", "postgres", "sentry", "axiom", "grafana", "redis"];
|
|
123288
|
+
var SUPPORTED = ["elasticsearch", "mongodb", "postgres", "sentry", "axiom", "shopify", "grafana", "redis"];
|
|
122807
123289
|
async function runConnect(type, opts) {
|
|
122808
123290
|
if (type === "ai") {
|
|
122809
123291
|
return runConnectAi({
|
|
@@ -122826,6 +123308,15 @@ async function runConnect(type, opts) {
|
|
|
122826
123308
|
const root = findRepoRoot(cwd) ?? cwd;
|
|
122827
123309
|
const configPath = discoverLocalConfig(cwd) ?? localConfigPath(root);
|
|
122828
123310
|
const filled = await fillInteractive(connectorType, opts);
|
|
123311
|
+
if (missingRequired(connectorType, filled)) {
|
|
123312
|
+
console.error(
|
|
123313
|
+
import_picocolors44.default.red(`Missing required field(s) for ${connectorType} \u2014 nothing saved.`) + import_picocolors44.default.dim(
|
|
123314
|
+
`
|
|
123315
|
+
Re-run ${import_picocolors44.default.bold(`horus connect ${connectorType}`)} and fill every prompt, or pass them as flags (e.g. --secret for the token).`
|
|
123316
|
+
)
|
|
123317
|
+
);
|
|
123318
|
+
return 1;
|
|
123319
|
+
}
|
|
122829
123320
|
let patch;
|
|
122830
123321
|
switch (connectorType) {
|
|
122831
123322
|
case "elasticsearch":
|
|
@@ -122843,6 +123334,9 @@ async function runConnect(type, opts) {
|
|
|
122843
123334
|
case "axiom":
|
|
122844
123335
|
patch = buildAxiomPatch(filled);
|
|
122845
123336
|
break;
|
|
123337
|
+
case "shopify":
|
|
123338
|
+
patch = buildShopifyPatch(filled);
|
|
123339
|
+
break;
|
|
122846
123340
|
case "grafana":
|
|
122847
123341
|
patch = buildGrafanaPatch(filled);
|
|
122848
123342
|
break;
|
|
@@ -122993,6 +123487,21 @@ ${import_picocolors44.default.bold(`Connect ${type}`)} ${import_picocolors44.def
|
|
|
122993
123487
|
}
|
|
122994
123488
|
break;
|
|
122995
123489
|
}
|
|
123490
|
+
case "shopify": {
|
|
123491
|
+
filled.store = filled.store ?? await ask("Store name (subdomain only, e.g. acme-store)", "my-store");
|
|
123492
|
+
filled.apiVersion = filled.apiVersion ?? await ask("API version", "2025-10");
|
|
123493
|
+
if (filled.secret === void 0) {
|
|
123494
|
+
const method = await askShopifyAuthMethod();
|
|
123495
|
+
if (method === "client-credentials") {
|
|
123496
|
+
filled.accessId = filled.accessId ?? await askRequiredText("Access id (client_id)");
|
|
123497
|
+
filled.secret = await askPassword("Client secret", true) || void 0;
|
|
123498
|
+
} else {
|
|
123499
|
+
filled.accessId = void 0;
|
|
123500
|
+
filled.secret = await askPassword("Admin API access token (e.g. shpat_\u2026)", true) || void 0;
|
|
123501
|
+
}
|
|
123502
|
+
}
|
|
123503
|
+
break;
|
|
123504
|
+
}
|
|
122996
123505
|
case "grafana":
|
|
122997
123506
|
filled.url = filled.url ?? await ask("URL", "https://grafana.example.com");
|
|
122998
123507
|
filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
|
|
@@ -123052,6 +123561,8 @@ function missingRequired(type, opts) {
|
|
|
123052
123561
|
return !opts.org || !opts.project || !opts.authToken;
|
|
123053
123562
|
case "axiom":
|
|
123054
123563
|
return !opts.dataset || !opts.token;
|
|
123564
|
+
case "shopify":
|
|
123565
|
+
return !opts.store || !opts.secret;
|
|
123055
123566
|
case "grafana":
|
|
123056
123567
|
return !opts.url;
|
|
123057
123568
|
case "redis":
|
|
@@ -123074,17 +123585,25 @@ function ask(label, placeholder = "", required2 = true) {
|
|
|
123074
123585
|
});
|
|
123075
123586
|
});
|
|
123076
123587
|
}
|
|
123077
|
-
function askPassword(label) {
|
|
123588
|
+
function askPassword(label, required2 = false) {
|
|
123078
123589
|
return new Promise((resolve13) => {
|
|
123079
123590
|
const stdin = process.stdin;
|
|
123080
123591
|
if (typeof stdin.setRawMode === "function") {
|
|
123081
|
-
|
|
123592
|
+
const suffix = required2 ? "" : import_picocolors44.default.dim(" [optional]");
|
|
123593
|
+
process.stdout.write(` ${label}${suffix}: `);
|
|
123082
123594
|
stdin.setRawMode(true);
|
|
123083
123595
|
stdin.resume();
|
|
123084
123596
|
let value = "";
|
|
123085
123597
|
const onData = (chunk) => {
|
|
123086
123598
|
const char2 = chunk.toString();
|
|
123087
123599
|
if (char2 === "\r" || char2 === "\n") {
|
|
123600
|
+
if (required2 && value === "") {
|
|
123601
|
+
process.stdout.write(`
|
|
123602
|
+
${import_picocolors44.default.yellow(" Required \u2014 please enter a value.")}
|
|
123603
|
+
`);
|
|
123604
|
+
process.stdout.write(` ${label}: `);
|
|
123605
|
+
return;
|
|
123606
|
+
}
|
|
123088
123607
|
stdin.setRawMode(false);
|
|
123089
123608
|
stdin.pause();
|
|
123090
123609
|
stdin.removeListener("data", onData);
|
|
@@ -123108,6 +123627,24 @@ function askPassword(label) {
|
|
|
123108
123627
|
}
|
|
123109
123628
|
});
|
|
123110
123629
|
}
|
|
123630
|
+
async function askRequiredText(label) {
|
|
123631
|
+
for (; ; ) {
|
|
123632
|
+
const v4 = (await ask(label, "", false)).trim();
|
|
123633
|
+
if (v4) return v4;
|
|
123634
|
+
console.log(import_picocolors44.default.yellow(" Required \u2014 please enter a value."));
|
|
123635
|
+
}
|
|
123636
|
+
}
|
|
123637
|
+
async function askShopifyAuthMethod() {
|
|
123638
|
+
console.log(` ${import_picocolors44.default.bold("Auth method")}`);
|
|
123639
|
+
console.log(` ${import_picocolors44.default.bold("1)")} Static Admin API access token ${import_picocolors44.default.dim("\u2014 a Custom App shpat_\u2026 token")}`);
|
|
123640
|
+
console.log(` ${import_picocolors44.default.bold("2)")} Client-Credentials app ${import_picocolors44.default.dim("\u2014 client id + secret, exchanged for a token")}`);
|
|
123641
|
+
for (; ; ) {
|
|
123642
|
+
const a2 = (await ask("Choose", "1")).trim();
|
|
123643
|
+
if (a2 === "1") return "static";
|
|
123644
|
+
if (a2 === "2") return "client-credentials";
|
|
123645
|
+
console.log(import_picocolors44.default.yellow(" Enter 1 or 2."));
|
|
123646
|
+
}
|
|
123647
|
+
}
|
|
123111
123648
|
function buildEsPatch(opts) {
|
|
123112
123649
|
if (!opts.indexPattern && !opts.indexPatterns?.length) {
|
|
123113
123650
|
throw new Error("Index pattern is required for elasticsearch");
|
|
@@ -123157,6 +123694,14 @@ function buildAxiomPatch(opts) {
|
|
|
123157
123694
|
if (opts.url && opts.url.replace(/\/$/, "") !== "https://api.axiom.co") patch["url"] = opts.url;
|
|
123158
123695
|
return patch;
|
|
123159
123696
|
}
|
|
123697
|
+
function buildShopifyPatch(opts) {
|
|
123698
|
+
if (!opts.store) throw new Error("Store is required for shopify");
|
|
123699
|
+
const patch = { store: opts.store };
|
|
123700
|
+
if (opts.apiVersion) patch["apiVersion"] = opts.apiVersion;
|
|
123701
|
+
if (opts.accessId) patch["accessId"] = opts.accessId;
|
|
123702
|
+
if (opts.secret) patch["secret"] = opts.secret;
|
|
123703
|
+
return patch;
|
|
123704
|
+
}
|
|
123160
123705
|
function buildGrafanaPatch(opts) {
|
|
123161
123706
|
const patch = {};
|
|
123162
123707
|
if (opts.url) patch["url"] = opts.url;
|
|
@@ -123303,6 +123848,18 @@ async function probe(type, opts) {
|
|
|
123303
123848
|
});
|
|
123304
123849
|
return client.health();
|
|
123305
123850
|
}
|
|
123851
|
+
case "shopify": {
|
|
123852
|
+
if (!opts.store || !opts.secret) {
|
|
123853
|
+
return { ok: true, detail: "skipped (missing store/secret)" };
|
|
123854
|
+
}
|
|
123855
|
+
const client = new ShopifyAdminClient({
|
|
123856
|
+
store: opts.store,
|
|
123857
|
+
secret: opts.secret,
|
|
123858
|
+
...opts.accessId ? { accessId: opts.accessId } : {},
|
|
123859
|
+
...opts.apiVersion ? { apiVersion: opts.apiVersion } : {}
|
|
123860
|
+
});
|
|
123861
|
+
return client.health();
|
|
123862
|
+
}
|
|
123306
123863
|
case "grafana": {
|
|
123307
123864
|
if (!opts.url) return { ok: true, detail: "skipped (no URL)" };
|
|
123308
123865
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -123355,6 +123912,11 @@ function printSummary(type, opts) {
|
|
|
123355
123912
|
if (opts.org) lines.push(` org: ${opts.org}`);
|
|
123356
123913
|
if (opts.project) lines.push(` project: ${opts.project}`);
|
|
123357
123914
|
if (opts.dataset) lines.push(` dataset: ${opts.dataset}`);
|
|
123915
|
+
if (opts.store) lines.push(` store: ${opts.store}`);
|
|
123916
|
+
if (opts.apiVersion) lines.push(` api-version: ${opts.apiVersion}`);
|
|
123917
|
+
if (opts.accessId) lines.push(` access-id: ${opts.accessId}`);
|
|
123918
|
+
if (opts.secret)
|
|
123919
|
+
lines.push(` secret: ${"\u2022".repeat(Math.min(opts.secret.length, 8))}`);
|
|
123358
123920
|
if (opts.authToken)
|
|
123359
123921
|
lines.push(` auth-token: ${"\u2022".repeat(Math.min(opts.authToken.length, 8))}`);
|
|
123360
123922
|
if (opts.token)
|
|
@@ -123750,13 +124312,13 @@ async function runNotify(sub, opts) {
|
|
|
123750
124312
|
|
|
123751
124313
|
// ../../packages/cli/src/commands/secrets.ts
|
|
123752
124314
|
init_cjs_shims();
|
|
123753
|
-
var
|
|
124315
|
+
var import_node_fs23 = require("fs");
|
|
123754
124316
|
var import_picocolors46 = __toESM(require_picocolors(), 1);
|
|
123755
124317
|
init_src();
|
|
123756
124318
|
function locate2(cwd = process.cwd()) {
|
|
123757
124319
|
const root = findRepoRoot(cwd) ?? cwd;
|
|
123758
124320
|
const configPath = discoverLocalConfig(cwd) ?? localConfigPath(root);
|
|
123759
|
-
return { root, configPath, exists: (0,
|
|
124321
|
+
return { root, configPath, exists: (0, import_node_fs23.existsSync)(configPath) };
|
|
123760
124322
|
}
|
|
123761
124323
|
function countStoredSecrets(root) {
|
|
123762
124324
|
const stored = readLocalSecrets(root).connectors;
|
|
@@ -123894,19 +124456,19 @@ export HORUS_SECRET_KEY=${key.toString("base64")}
|
|
|
123894
124456
|
// ../../packages/cli/src/commands/stop.ts
|
|
123895
124457
|
init_cjs_shims();
|
|
123896
124458
|
var import_node_child_process10 = require("child_process");
|
|
123897
|
-
var
|
|
124459
|
+
var import_node_fs24 = require("fs");
|
|
123898
124460
|
var import_node_util6 = require("util");
|
|
123899
|
-
var
|
|
124461
|
+
var import_node_path26 = require("path");
|
|
123900
124462
|
var import_picocolors47 = __toESM(require_picocolors(), 1);
|
|
123901
124463
|
init_src();
|
|
123902
124464
|
var execFileAsync3 = (0, import_node_util6.promisify)(import_node_child_process10.execFile);
|
|
123903
|
-
var unlinkAsync = (0, import_node_util6.promisify)(
|
|
124465
|
+
var unlinkAsync = (0, import_node_util6.promisify)(import_node_fs24.unlink);
|
|
123904
124466
|
var SPAWNED_HOST_FILE2 = "spawned-host.json";
|
|
123905
124467
|
var START_TIME_TOLERANCE_S = 60;
|
|
123906
124468
|
var STOP_WAIT_MS = 5e3;
|
|
123907
124469
|
var STOP_KILL_WAIT_MS = 3e3;
|
|
123908
124470
|
var STOP_POLL_MS = 200;
|
|
123909
|
-
var
|
|
124471
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
123910
124472
|
async function runStop(opts) {
|
|
123911
124473
|
try {
|
|
123912
124474
|
if (opts.all) {
|
|
@@ -124042,11 +124604,11 @@ function isOwnHostProcess(pid, port, root, args2) {
|
|
|
124042
124604
|
if (hostArgvRegex(port).test(args2)) return true;
|
|
124043
124605
|
if (!hostArgvAnyPortRegex().test(args2)) return false;
|
|
124044
124606
|
const rec = readSourceHostPid(root);
|
|
124045
|
-
return rec !== null && rec.pid === pid && Number.isFinite(rec.port) && rec.port === port && !!rec.repoPath && (0,
|
|
124607
|
+
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);
|
|
124046
124608
|
}
|
|
124047
124609
|
async function cleanupSpawnedRecord(root) {
|
|
124048
124610
|
try {
|
|
124049
|
-
await unlinkAsync((0,
|
|
124611
|
+
await unlinkAsync((0, import_node_path26.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
|
|
124050
124612
|
} catch {
|
|
124051
124613
|
}
|
|
124052
124614
|
}
|
|
@@ -124150,7 +124712,7 @@ async function terminatePid(pid) {
|
|
|
124150
124712
|
}
|
|
124151
124713
|
const deadline = Date.now() + waitMs;
|
|
124152
124714
|
while (Date.now() < deadline) {
|
|
124153
|
-
await
|
|
124715
|
+
await sleep2(STOP_POLL_MS);
|
|
124154
124716
|
if (await getProcessInfo(pid) === null) return true;
|
|
124155
124717
|
}
|
|
124156
124718
|
}
|
|
@@ -124470,6 +125032,29 @@ var CONNECTOR_CHECKS = {
|
|
|
124470
125032
|
};
|
|
124471
125033
|
}
|
|
124472
125034
|
},
|
|
125035
|
+
// Shopify (Admin GraphQL — application state evidence)
|
|
125036
|
+
shopify: {
|
|
125037
|
+
label: "Shopify",
|
|
125038
|
+
absentNext: "add connectors.shopify to an environment for Shopify Admin state evidence",
|
|
125039
|
+
check: (s5, ctx) => {
|
|
125040
|
+
const secretSet = s5.secret || process.env[s5.secretEnv ?? "SHOPIFY_SECRET"];
|
|
125041
|
+
if (!secretSet) {
|
|
125042
|
+
return {
|
|
125043
|
+
label: "Shopify",
|
|
125044
|
+
status: "warn",
|
|
125045
|
+
detail: `${ctx} \u2014 client secret not set`,
|
|
125046
|
+
next: "set shopify.secret or shopify.secretEnv (default SHOPIFY_SECRET), or run horus connect shopify"
|
|
125047
|
+
};
|
|
125048
|
+
}
|
|
125049
|
+
const queryCount = s5.queries?.length ?? 0;
|
|
125050
|
+
const queryNote = queryCount > 0 ? ` (${queryCount} default quer${queryCount === 1 ? "y" : "ies"})` : " (no default queries \u2014 supply --shopify-query at investigate time)";
|
|
125051
|
+
return {
|
|
125052
|
+
label: "Shopify",
|
|
125053
|
+
status: "pass",
|
|
125054
|
+
detail: `${ctx} \u2014 ${s5.store}${queryNote} [runtime ingestion pending]`
|
|
125055
|
+
};
|
|
125056
|
+
}
|
|
125057
|
+
},
|
|
124473
125058
|
// Redis / BullMQ (queue + cache/state)
|
|
124474
125059
|
redis: {
|
|
124475
125060
|
label: "Redis",
|
|
@@ -124752,8 +125337,8 @@ async function runProvidersDoctorCommand(opts) {
|
|
|
124752
125337
|
|
|
124753
125338
|
// ../../packages/cli/src/commands/generate-config.ts
|
|
124754
125339
|
init_cjs_shims();
|
|
124755
|
-
var
|
|
124756
|
-
var
|
|
125340
|
+
var import_node_fs25 = require("fs");
|
|
125341
|
+
var import_node_path27 = require("path");
|
|
124757
125342
|
var import_picocolors51 = __toESM(require_picocolors(), 1);
|
|
124758
125343
|
init_src();
|
|
124759
125344
|
function configTemplate(name2, repoPath) {
|
|
@@ -124775,6 +125360,16 @@ function configTemplate(name2, repoPath) {
|
|
|
124775
125360
|
// elasticsearch: { indexPattern: '${name2}-prod-*' },
|
|
124776
125361
|
// mongodb: { database: '${name2}' },
|
|
124777
125362
|
// grafana: {},
|
|
125363
|
+
// shopify: {
|
|
125364
|
+
// store: '${name2}', // subdomain only \u2014 .myshopify.com is added automatically
|
|
125365
|
+
// apiVersion: '2025-10',
|
|
125366
|
+
// // accessId + secret set by \`horus connect shopify\` (secret encrypted at rest).
|
|
125367
|
+
// // Default queries run by \`horus watch\`; \`horus investigate --shopify-query\` overrides.
|
|
125368
|
+
// queries: [
|
|
125369
|
+
// { name: 'recent-orders', kind: 'state', bindWindow: true,
|
|
125370
|
+
// query: 'query($from:DateTime){ orders(first:20, query:"updated_at:>=$from"){ edges{ node{ id name displayFinancialStatus } } } }' },
|
|
125371
|
+
// ],
|
|
125372
|
+
// },
|
|
124778
125373
|
},
|
|
124779
125374
|
}],
|
|
124780
125375
|
}],
|
|
@@ -124803,17 +125398,17 @@ async function runGenerateConfig(opts) {
|
|
|
124803
125398
|
const defaults2 = localConfigPath2 != null ? projectDefaults(localConfigPath2) : null;
|
|
124804
125399
|
const hasLocalConfig = defaults2 != null;
|
|
124805
125400
|
const defaultOut = hasLocalConfig ? "horus.config.example.js" : "horus.config.js";
|
|
124806
|
-
const outPath = (0,
|
|
125401
|
+
const outPath = (0, import_node_path27.resolve)(cwd, opts.out ?? defaultOut);
|
|
124807
125402
|
const name2 = opts.name ?? defaults2?.name ?? "my-project";
|
|
124808
125403
|
const repoPath = opts.repo ?? defaults2?.repoPath ?? `/path/to/${name2}`;
|
|
124809
|
-
if ((0,
|
|
125404
|
+
if ((0, import_node_fs25.existsSync)(outPath) && !opts.force) {
|
|
124810
125405
|
log(`${import_picocolors51.default.red("\u2717")} ${outPath} already exists`);
|
|
124811
125406
|
log(import_picocolors51.default.dim(" pass --force to overwrite"));
|
|
124812
125407
|
return 1;
|
|
124813
125408
|
}
|
|
124814
125409
|
try {
|
|
124815
|
-
(0,
|
|
124816
|
-
(0,
|
|
125410
|
+
(0, import_node_fs25.mkdirSync)((0, import_node_path27.dirname)(outPath), { recursive: true });
|
|
125411
|
+
(0, import_node_fs25.writeFileSync)(outPath, configTemplate(name2, repoPath), "utf8");
|
|
124817
125412
|
} catch (err2) {
|
|
124818
125413
|
log(`${import_picocolors51.default.red("\u2717")} Could not write ${outPath}: ${err2.message}`);
|
|
124819
125414
|
return 1;
|
|
@@ -124975,6 +125570,7 @@ async function runReadiness(opts) {
|
|
|
124975
125570
|
let anyPostgres = false;
|
|
124976
125571
|
let anySentry = false;
|
|
124977
125572
|
let anyAxiom = false;
|
|
125573
|
+
let anyShopify = false;
|
|
124978
125574
|
let anyRedis = false;
|
|
124979
125575
|
for (const project of globalConfig2.projects) {
|
|
124980
125576
|
for (const repo of project.repositories) {
|
|
@@ -124988,6 +125584,7 @@ async function runReadiness(opts) {
|
|
|
124988
125584
|
if (c4.postgres) anyPostgres = true;
|
|
124989
125585
|
if (c4.sentry) anySentry = true;
|
|
124990
125586
|
if (c4.axiom) anyAxiom = true;
|
|
125587
|
+
if (c4.shopify) anyShopify = true;
|
|
124991
125588
|
if (c4.redis) anyRedis = true;
|
|
124992
125589
|
}
|
|
124993
125590
|
}
|
|
@@ -125054,6 +125651,15 @@ async function runReadiness(opts) {
|
|
|
125054
125651
|
next: "run `horus connect axiom`"
|
|
125055
125652
|
}
|
|
125056
125653
|
);
|
|
125654
|
+
checks.push(
|
|
125655
|
+
anyShopify ? { label: "Shopify", status: "pass", blocking: false, detail: "configured" } : {
|
|
125656
|
+
label: "Shopify",
|
|
125657
|
+
status: "warn",
|
|
125658
|
+
blocking: false,
|
|
125659
|
+
detail: "not configured \u2014 no Shopify Admin state evidence",
|
|
125660
|
+
next: "run `horus connect shopify`"
|
|
125661
|
+
}
|
|
125662
|
+
);
|
|
125057
125663
|
checks.push(
|
|
125058
125664
|
anyRedis ? { label: "Redis", status: "pass", blocking: false, detail: "configured" } : {
|
|
125059
125665
|
label: "Redis",
|
|
@@ -125124,8 +125730,8 @@ async function runReadiness(opts) {
|
|
|
125124
125730
|
|
|
125125
125731
|
// ../../packages/cli/src/commands/skill.ts
|
|
125126
125732
|
init_cjs_shims();
|
|
125127
|
-
var
|
|
125128
|
-
var
|
|
125733
|
+
var import_node_fs26 = require("fs");
|
|
125734
|
+
var import_node_path28 = require("path");
|
|
125129
125735
|
var import_node_os10 = require("os");
|
|
125130
125736
|
var import_promises3 = require("readline/promises");
|
|
125131
125737
|
var import_picocolors53 = __toESM(require_picocolors(), 1);
|
|
@@ -125135,22 +125741,22 @@ function isValidTarget(target) {
|
|
|
125135
125741
|
}
|
|
125136
125742
|
function getLegacyClaudeSkillPath(opts) {
|
|
125137
125743
|
const base2 = opts.global ? (0, import_node_os10.homedir)() : opts.cwd ?? process.cwd();
|
|
125138
|
-
return (0,
|
|
125744
|
+
return (0, import_node_path28.join)(base2, ".claude", "skills", "horus.md");
|
|
125139
125745
|
}
|
|
125140
125746
|
function getSkillInstallPath(target, opts) {
|
|
125141
125747
|
const base2 = opts.global ? (0, import_node_os10.homedir)() : opts.cwd ?? process.cwd();
|
|
125142
125748
|
switch (target) {
|
|
125143
125749
|
case "claude":
|
|
125144
|
-
return (0,
|
|
125750
|
+
return (0, import_node_path28.join)(base2, ".claude", "skills", "horus", "SKILL.md");
|
|
125145
125751
|
case "gemini":
|
|
125146
|
-
return opts.global ? (0,
|
|
125752
|
+
return opts.global ? (0, import_node_path28.join)(base2, ".gemini", "GEMINI.md") : (0, import_node_path28.join)(base2, "GEMINI.md");
|
|
125147
125753
|
case "cursor":
|
|
125148
|
-
return opts.global ? (0,
|
|
125754
|
+
return opts.global ? (0, import_node_path28.join)(base2, ".cursorrules") : (0, import_node_path28.join)(base2, ".cursor", "rules", "horus.mdc");
|
|
125149
125755
|
case "codex":
|
|
125150
|
-
return opts.global ? (0,
|
|
125756
|
+
return opts.global ? (0, import_node_path28.join)(base2, ".codex", "AGENTS.md") : (0, import_node_path28.join)(base2, "AGENTS.md");
|
|
125151
125757
|
case "generic":
|
|
125152
125758
|
default:
|
|
125153
|
-
return (0,
|
|
125759
|
+
return (0, import_node_path28.join)(base2, ".horus", "skills", `horus-${target}.md`);
|
|
125154
125760
|
}
|
|
125155
125761
|
}
|
|
125156
125762
|
var BASE_SKILL = `# Horus Grounded Investigation Skill
|
|
@@ -125475,7 +126081,7 @@ async function runSkillInstall(target, opts) {
|
|
|
125475
126081
|
const content = generateSkillContent(target);
|
|
125476
126082
|
if (target === "claude") {
|
|
125477
126083
|
const legacyPath = getLegacyClaudeSkillPath({ global: opts.global, cwd: opts.cwd });
|
|
125478
|
-
if ((0,
|
|
126084
|
+
if ((0, import_node_fs26.existsSync)(legacyPath)) {
|
|
125479
126085
|
if (!opts.force) {
|
|
125480
126086
|
const confirmed = await (opts.confirm ?? confirm)(
|
|
125481
126087
|
`Found old flat skill file at ${legacyPath}. Replace it with the new directory layout?`
|
|
@@ -125487,7 +126093,7 @@ async function runSkillInstall(target, opts) {
|
|
|
125487
126093
|
}
|
|
125488
126094
|
}
|
|
125489
126095
|
try {
|
|
125490
|
-
(0,
|
|
126096
|
+
(0, import_node_fs26.rmSync)(legacyPath);
|
|
125491
126097
|
log(`${import_picocolors53.default.yellow("!")} Replaced old flat skill file: ${legacyPath}`);
|
|
125492
126098
|
} catch (err2) {
|
|
125493
126099
|
log(`${import_picocolors53.default.red("\u2717")} Could not remove legacy skill file ${legacyPath}: ${err2.message}`);
|
|
@@ -125495,14 +126101,14 @@ async function runSkillInstall(target, opts) {
|
|
|
125495
126101
|
}
|
|
125496
126102
|
}
|
|
125497
126103
|
}
|
|
125498
|
-
if ((0,
|
|
126104
|
+
if ((0, import_node_fs26.existsSync)(outPath) && !opts.force) {
|
|
125499
126105
|
log(`${import_picocolors53.default.red("\u2717")} ${outPath} already exists`);
|
|
125500
126106
|
log(import_picocolors53.default.dim(" pass --force to overwrite"));
|
|
125501
126107
|
return 1;
|
|
125502
126108
|
}
|
|
125503
126109
|
try {
|
|
125504
|
-
(0,
|
|
125505
|
-
(0,
|
|
126110
|
+
(0, import_node_fs26.mkdirSync)((0, import_node_path28.dirname)(outPath), { recursive: true });
|
|
126111
|
+
(0, import_node_fs26.writeFileSync)(outPath, content, "utf8");
|
|
125506
126112
|
} catch (err2) {
|
|
125507
126113
|
log(`${import_picocolors53.default.red("\u2717")} Could not write ${outPath}: ${err2.message}`);
|
|
125508
126114
|
return 1;
|
|
@@ -125547,7 +126153,7 @@ async function runSkillPath(target, opts) {
|
|
|
125547
126153
|
// ../../packages/cli/src/commands/login.ts
|
|
125548
126154
|
init_cjs_shims();
|
|
125549
126155
|
var import_picocolors54 = __toESM(require_picocolors(), 1);
|
|
125550
|
-
var
|
|
126156
|
+
var sleep3 = (ms) => new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
125551
126157
|
async function loginWithToken(apiBaseUrl, token) {
|
|
125552
126158
|
const client = new CloudClient(apiBaseUrl, token);
|
|
125553
126159
|
try {
|
|
@@ -125602,7 +126208,7 @@ async function loginWithBrowser(apiBaseUrl) {
|
|
|
125602
126208
|
const deadline = Date.now() + session.expiresIn * 1e3;
|
|
125603
126209
|
let intervalMs = Math.max(session.interval, 1) * 1e3;
|
|
125604
126210
|
while (Date.now() < deadline) {
|
|
125605
|
-
await
|
|
126211
|
+
await sleep3(intervalMs);
|
|
125606
126212
|
let poll;
|
|
125607
126213
|
try {
|
|
125608
126214
|
poll = await client.pollCliSession(session.deviceCode);
|
|
@@ -126243,7 +126849,7 @@ function installCommandTelemetry(program2) {
|
|
|
126243
126849
|
// ../../packages/cli/src/commands/eval.ts
|
|
126244
126850
|
init_cjs_shims();
|
|
126245
126851
|
var import_promises5 = require("fs/promises");
|
|
126246
|
-
var
|
|
126852
|
+
var import_node_path29 = require("path");
|
|
126247
126853
|
var import_picocolors58 = __toESM(require_picocolors(), 1);
|
|
126248
126854
|
init_src();
|
|
126249
126855
|
function resolveProject2(config2, repo) {
|
|
@@ -126294,7 +126900,7 @@ async function runEvalBuild(opts) {
|
|
|
126294
126900
|
const project = resolveProject2(config2, opts.repo);
|
|
126295
126901
|
if (!project) return 1;
|
|
126296
126902
|
const version3 = opts.version ?? CORPUS_VERSION;
|
|
126297
|
-
const outDir = opts.out ? (0,
|
|
126903
|
+
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");
|
|
126298
126904
|
const { db, sql: sql2 } = await openDb(config2.database.url);
|
|
126299
126905
|
try {
|
|
126300
126906
|
const labels = await listOutcomeLabels(db, {
|
|
@@ -126310,8 +126916,8 @@ async function runEvalBuild(opts) {
|
|
|
126310
126916
|
const build = buildCorpus(labels, resolveReport);
|
|
126311
126917
|
const artifact = serializeCorpus(build, version3);
|
|
126312
126918
|
await (0, import_promises5.mkdir)(outDir, { recursive: true });
|
|
126313
|
-
const corpusPath = (0,
|
|
126314
|
-
const manifestPath = (0,
|
|
126919
|
+
const corpusPath = (0, import_node_path29.join)(outDir, artifact.filename);
|
|
126920
|
+
const manifestPath = (0, import_node_path29.join)(outDir, `corpus-${version3}.manifest.json`);
|
|
126315
126921
|
const manifest = { ...artifact.manifest, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), project };
|
|
126316
126922
|
await (0, import_promises5.writeFile)(corpusPath, artifact.jsonl, "utf8");
|
|
126317
126923
|
await (0, import_promises5.writeFile)(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
@@ -126412,11 +127018,11 @@ async function runEvalBaseline(opts) {
|
|
|
126412
127018
|
// ../../packages/cli/src/commands/train.ts
|
|
126413
127019
|
init_cjs_shims();
|
|
126414
127020
|
var import_promises6 = require("fs/promises");
|
|
126415
|
-
var
|
|
127021
|
+
var import_node_path30 = require("path");
|
|
126416
127022
|
var import_node_os11 = require("os");
|
|
126417
127023
|
var import_picocolors59 = __toESM(require_picocolors(), 1);
|
|
126418
127024
|
init_src();
|
|
126419
|
-
var RERANKER_MODEL_PATH = (0,
|
|
127025
|
+
var RERANKER_MODEL_PATH = (0, import_node_path30.join)((0, import_node_os11.homedir)(), ".horus", "reranker.json");
|
|
126420
127026
|
function candidatesFromReport(blob) {
|
|
126421
127027
|
if (blob === null || typeof blob !== "object") return null;
|
|
126422
127028
|
const causes = blob.suspectedCauses;
|
|
@@ -126505,7 +127111,7 @@ async function runTrain(opts) {
|
|
|
126505
127111
|
return 0;
|
|
126506
127112
|
}
|
|
126507
127113
|
try {
|
|
126508
|
-
await (0, import_promises6.mkdir)((0,
|
|
127114
|
+
await (0, import_promises6.mkdir)((0, import_node_path30.join)((0, import_node_os11.homedir)(), ".horus"), { recursive: true });
|
|
126509
127115
|
const { ok: _ok, ...model } = result;
|
|
126510
127116
|
void _ok;
|
|
126511
127117
|
await (0, import_promises6.writeFile)(RERANKER_MODEL_PATH, JSON.stringify(model, null, 2) + "\n", "utf8");
|
|
@@ -126573,8 +127179,8 @@ Examples:
|
|
|
126573
127179
|
process.exitCode = await runReadiness({ config: opts.config, ai: opts.ai, aiModel: opts.aiModel });
|
|
126574
127180
|
});
|
|
126575
127181
|
program2.command("connect <type>").description(
|
|
126576
|
-
"Add or update a connector (elasticsearch / mongodb / postgres / sentry / axiom / grafana / redis / ai) in .horus/config.json"
|
|
126577
|
-
).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(
|
|
127182
|
+
"Add or update a connector (elasticsearch / mongodb / postgres / sentry / axiom / shopify / grafana / redis / ai) in .horus/config.json"
|
|
127183
|
+
).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(
|
|
126578
127184
|
"--db <spec>",
|
|
126579
127185
|
"redis logical DB as db:role1,role2 (e.g. 0:cache,state or 1:bullmq,queues); repeatable",
|
|
126580
127186
|
(val, acc) => {
|
|
@@ -126600,6 +127206,10 @@ Examples:
|
|
|
126600
127206
|
project: opts.project,
|
|
126601
127207
|
token: opts.token,
|
|
126602
127208
|
dataset: opts.dataset,
|
|
127209
|
+
store: opts.store,
|
|
127210
|
+
apiVersion: opts.apiVersion,
|
|
127211
|
+
accessId: opts.accessId,
|
|
127212
|
+
secret: opts.secret,
|
|
126603
127213
|
dashboard: opts.dashboard,
|
|
126604
127214
|
db: opts.db,
|
|
126605
127215
|
bullmqPrefix: opts.bullmqPrefix,
|
|
@@ -126779,7 +127389,15 @@ score, a confidence, or a verdict. It ships OFF; once trained, enable with:
|
|
|
126779
127389
|
});
|
|
126780
127390
|
}
|
|
126781
127391
|
);
|
|
126782
|
-
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(
|
|
127392
|
+
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(
|
|
127393
|
+
"--shopify-query <q>",
|
|
127394
|
+
"Shopify Admin GraphQL query to run as evidence: @file, - (stdin), or a raw string; repeatable",
|
|
127395
|
+
(val, acc) => {
|
|
127396
|
+
acc.push(val);
|
|
127397
|
+
return acc;
|
|
127398
|
+
},
|
|
127399
|
+
[]
|
|
127400
|
+
).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(
|
|
126783
127401
|
"--service <name>",
|
|
126784
127402
|
"service name to scope runtime logs, e.g. leadcall-api-prod"
|
|
126785
127403
|
).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(
|
|
@@ -126793,6 +127411,8 @@ score, a confidence, or a verdict. It ships OFF; once trained, enable with:
|
|
|
126793
127411
|
scope: opts.scope,
|
|
126794
127412
|
since: opts.since,
|
|
126795
127413
|
logsSince: opts.logsSince,
|
|
127414
|
+
...opts.shopifyQuery !== void 0 ? { shopifyQuery: opts.shopifyQuery } : {},
|
|
127415
|
+
...opts.shopifyVariables !== void 0 ? { shopifyVariables: opts.shopifyVariables } : {},
|
|
126796
127416
|
timeout: opts.timeout,
|
|
126797
127417
|
service: opts.service,
|
|
126798
127418
|
json: opts.json,
|