@hasna/instructions 0.5.5 → 0.6.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 +64 -20
- package/dist/cli/fail-closed-no-env.test.d.ts +2 -0
- package/dist/cli/fail-closed-no-env.test.d.ts.map +1 -0
- package/dist/cli/index.js +1910 -561
- package/dist/data/config-store.d.ts +62 -33
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/generated/storage-kit/backend.d.ts +4 -4
- package/dist/generated/storage-kit/backend.d.ts.map +1 -1
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/generated/storage-kit/index.d.ts.map +1 -1
- package/dist/generated/storage-kit/migrations.d.ts +21 -0
- package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
- package/dist/generated/storage-kit/pool.d.ts +2 -5
- package/dist/generated/storage-kit/pool.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2702 -547
- package/dist/lib/app-home.d.ts +9 -0
- package/dist/lib/app-home.d.ts.map +1 -1
- package/dist/lib/client-types.d.ts +130 -0
- package/dist/lib/client-types.d.ts.map +1 -0
- package/dist/lib/client-types.test.d.ts +2 -0
- package/dist/lib/client-types.test.d.ts.map +1 -0
- package/dist/lib/local-opt-in.d.ts +82 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/project-context.d.ts +14 -14
- package/dist/lib/project-context.d.ts.map +1 -1
- package/dist/lib/project-dashboard-standard.d.ts +1 -1
- package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
- package/dist/lib/session-apply.d.ts.map +1 -1
- package/dist/lib/session-render-state-hermeticity.test.d.ts +2 -0
- package/dist/lib/session-render-state-hermeticity.test.d.ts.map +1 -0
- package/dist/lib/session-render-state.d.ts +57 -0
- package/dist/lib/session-render-state.d.ts.map +1 -0
- package/dist/lib/session-render-state.test.d.ts +2 -0
- package/dist/lib/session-render-state.test.d.ts.map +1 -0
- package/dist/lib/transport-resolver.d.ts +80 -0
- package/dist/lib/transport-resolver.d.ts.map +1 -0
- package/dist/lib/transport-resolver.test.d.ts +2 -0
- package/dist/lib/transport-resolver.test.d.ts.map +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1442 -192
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +22 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +1042 -0
- package/dist/sdk/resolve.d.ts +82 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/resolve.test.d.ts +2 -0
- package/dist/sdk/resolve.test.d.ts.map +1 -0
- package/dist/sdk/sdk-bundle-self-contained.test.d.ts +2 -0
- package/dist/sdk/sdk-bundle-self-contained.test.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +288 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -0
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +55 -64
- package/dist/test-support/preload-state-home.d.ts +2 -0
- package/dist/test-support/preload-state-home.d.ts.map +1 -0
- package/package.json +12 -12
- package/dashboard/README.md +0 -37
- package/dist/lib/retired-storage-mode.d.ts +0 -8
- package/dist/lib/retired-storage-mode.d.ts.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -1290,8 +1290,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1290
1290
|
args = args.slice();
|
|
1291
1291
|
let launchWithNode = false;
|
|
1292
1292
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
1293
|
-
function findFile(
|
|
1294
|
-
const localBin = path.resolve(
|
|
1293
|
+
function findFile(baseDir, baseName) {
|
|
1294
|
+
const localBin = path.resolve(baseDir, baseName);
|
|
1295
1295
|
if (fs.existsSync(localBin))
|
|
1296
1296
|
return localBin;
|
|
1297
1297
|
if (sourceExt.includes(path.extname(baseName)))
|
|
@@ -2136,116 +2136,167 @@ var init_types = __esm(() => {
|
|
|
2136
2136
|
};
|
|
2137
2137
|
});
|
|
2138
2138
|
|
|
2139
|
-
//
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
return key;
|
|
2144
|
-
}
|
|
2145
|
-
return null;
|
|
2139
|
+
// ../contracts/dist/client/transport.js
|
|
2140
|
+
import { createRequire } from "module";
|
|
2141
|
+
function envToken(name) {
|
|
2142
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
2146
2143
|
}
|
|
2147
|
-
function
|
|
2148
|
-
const
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2144
|
+
function clientTransportEnvKeys(name) {
|
|
2145
|
+
const envSegment = envToken(name);
|
|
2146
|
+
return {
|
|
2147
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
2148
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
function credentialOverrideEnvKey(name) {
|
|
2152
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
2153
|
+
}
|
|
2154
|
+
function credentialPointerEnvKey(name) {
|
|
2155
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
2156
|
+
}
|
|
2157
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", MAX_CREDENTIAL_FILE_BYTES, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
|
|
2158
|
+
var init_transport = __esm(() => {
|
|
2159
|
+
MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
2160
|
+
INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
2161
|
+
CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
2162
|
+
AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
2163
|
+
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
2164
|
+
requireSecretsSdk = createRequire(import.meta.url);
|
|
2165
|
+
IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
2166
|
+
AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
2167
|
+
"host",
|
|
2168
|
+
":authority",
|
|
2169
|
+
"forwarded",
|
|
2170
|
+
"x-forwarded-host",
|
|
2171
|
+
"x-original-host"
|
|
2172
|
+
]);
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
// src/lib/local-opt-in.ts
|
|
2176
|
+
function instructionsLocalModeNotice() {
|
|
2177
|
+
return `instructions: local mode \u2014 ${INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 selects the on-box SQLite store, ` + `and no hosted authority was configured. Set HASNA_INSTRUCTIONS_API_KEY (or add the Keychain item ` + `hasna.credentials.instructions.api-key, or write ~/.hasna/instructions/config/credentials) to go hosted.`;
|
|
2178
|
+
}
|
|
2179
|
+
function isInstructionsLocalOptIn(env = process.env) {
|
|
2180
|
+
return INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() === "1");
|
|
2181
|
+
}
|
|
2182
|
+
function instructionsAuthorityEnvKeys() {
|
|
2183
|
+
const keys = clientTransportEnvKeys("instructions");
|
|
2184
|
+
return [
|
|
2185
|
+
...keys.apiUrlKeys,
|
|
2186
|
+
...keys.apiKeyKeys,
|
|
2187
|
+
credentialOverrideEnvKey("instructions"),
|
|
2188
|
+
credentialPointerEnvKey("instructions"),
|
|
2189
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
2160
2190
|
];
|
|
2191
|
+
}
|
|
2192
|
+
function hasInstructionsEnvAuthorityIntent(env = process.env) {
|
|
2193
|
+
return instructionsAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
|
|
2194
|
+
}
|
|
2195
|
+
function selectsInstructionsLocalStore(env = process.env) {
|
|
2196
|
+
return !hasInstructionsEnvAuthorityIntent(env) && isInstructionsLocalOptIn(env);
|
|
2197
|
+
}
|
|
2198
|
+
function instructionsResolverEnv(env) {
|
|
2199
|
+
const blanks = instructionsAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
2200
|
+
if (blanks.length === 0)
|
|
2201
|
+
return env;
|
|
2202
|
+
const next = { ...env };
|
|
2203
|
+
for (const key of blanks)
|
|
2204
|
+
delete next[key];
|
|
2205
|
+
return next;
|
|
2206
|
+
}
|
|
2207
|
+
function isAmbientInstructionsEnv(env) {
|
|
2208
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
2209
|
+
return true;
|
|
2210
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
2211
|
+
}
|
|
2212
|
+
function instructionsResolverInputs(env, credentials = {}) {
|
|
2213
|
+
const normalised = instructionsResolverEnv(env);
|
|
2214
|
+
if (normalised === env)
|
|
2215
|
+
return { env: normalised, credentials };
|
|
2216
|
+
const keychain = { ...credentials.keychain };
|
|
2217
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
2218
|
+
keychain.enabled = isAmbientInstructionsEnv(env);
|
|
2219
|
+
}
|
|
2220
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
2221
|
+
}
|
|
2222
|
+
var INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
|
|
2223
|
+
var init_local_opt_in = __esm(() => {
|
|
2224
|
+
init_transport();
|
|
2225
|
+
INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_INSTRUCTIONS_LOCAL"];
|
|
2226
|
+
CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
2161
2227
|
});
|
|
2162
2228
|
|
|
2163
|
-
//
|
|
2164
|
-
import {
|
|
2165
|
-
import {
|
|
2166
|
-
|
|
2229
|
+
// src/lib/app-home.ts
|
|
2230
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2231
|
+
import { homedir as homedir2 } from "os";
|
|
2232
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
2233
|
+
import { homedir as pathsResolverHomedir2 } from "os";
|
|
2234
|
+
import { join as pathsResolverJoin2 } from "path";
|
|
2235
|
+
function pathsResolverAssertApp2(app) {
|
|
2167
2236
|
if (typeof app !== "string" || app.length === 0) {
|
|
2168
2237
|
throw new TypeError("paths: app must be a non-empty string");
|
|
2169
2238
|
}
|
|
2170
|
-
if (!
|
|
2239
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE2.test(app)) {
|
|
2171
2240
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
2172
2241
|
}
|
|
2173
2242
|
}
|
|
2174
|
-
function
|
|
2175
|
-
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
const value = envOf2(options)[KIND_ENV2[kind]];
|
|
2179
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
2180
|
-
}
|
|
2181
|
-
function isMacOS2(platform) {
|
|
2182
|
-
return platform === "darwin";
|
|
2243
|
+
function pathsResolverAssertKind2(kind) {
|
|
2244
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV2).includes(kind)) {
|
|
2245
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV2).join(", ")}`);
|
|
2246
|
+
}
|
|
2183
2247
|
}
|
|
2184
|
-
function
|
|
2185
|
-
|
|
2186
|
-
|
|
2248
|
+
function pathsResolverBaseDir2(kind, options) {
|
|
2249
|
+
pathsResolverAssertKind2(kind);
|
|
2250
|
+
const env = options.env ?? process.env;
|
|
2251
|
+
const override = env[PATHS_RESOLVER_KIND_ENV2[kind]];
|
|
2252
|
+
if (typeof override === "string" && override.length > 0)
|
|
2187
2253
|
return override;
|
|
2188
|
-
const home = options.home ??
|
|
2254
|
+
const home = options.home ?? pathsResolverHomedir2();
|
|
2189
2255
|
const platform = options.platform ?? process.platform;
|
|
2190
|
-
if (
|
|
2256
|
+
if (platform === "darwin") {
|
|
2191
2257
|
switch (kind) {
|
|
2192
2258
|
case "config":
|
|
2193
2259
|
case "data":
|
|
2194
|
-
return
|
|
2260
|
+
return pathsResolverJoin2(home, "Library", "Application Support", "Hasna");
|
|
2195
2261
|
case "cache":
|
|
2196
|
-
return
|
|
2262
|
+
return pathsResolverJoin2(home, "Library", "Caches", "Hasna");
|
|
2197
2263
|
case "state":
|
|
2198
|
-
return
|
|
2264
|
+
return pathsResolverJoin2(home, "Library", "Logs", "Hasna");
|
|
2199
2265
|
}
|
|
2200
2266
|
}
|
|
2201
2267
|
switch (kind) {
|
|
2202
2268
|
case "config":
|
|
2203
|
-
return
|
|
2269
|
+
return pathsResolverJoin2(home, ".config", "hasna");
|
|
2204
2270
|
case "data":
|
|
2205
|
-
return
|
|
2271
|
+
return pathsResolverJoin2(home, ".local", "share", "hasna");
|
|
2206
2272
|
case "state":
|
|
2207
|
-
return
|
|
2273
|
+
return pathsResolverJoin2(home, ".local", "state", "hasna");
|
|
2208
2274
|
case "cache":
|
|
2209
|
-
return
|
|
2275
|
+
return pathsResolverJoin2(home, ".cache", "hasna");
|
|
2210
2276
|
}
|
|
2211
2277
|
}
|
|
2212
|
-
function
|
|
2213
|
-
|
|
2214
|
-
const appSegment = options.internal === true ?
|
|
2215
|
-
return
|
|
2278
|
+
function pathsResolverResolve2(kind, options) {
|
|
2279
|
+
pathsResolverAssertApp2(options.app);
|
|
2280
|
+
const appSegment = options.internal === true ? pathsResolverJoin2("internal", options.app) : options.app;
|
|
2281
|
+
return pathsResolverJoin2(pathsResolverBaseDir2(kind, options), appSegment);
|
|
2216
2282
|
}
|
|
2217
2283
|
function configDir(options) {
|
|
2218
|
-
return
|
|
2284
|
+
return pathsResolverResolve2("config", options);
|
|
2219
2285
|
}
|
|
2220
|
-
var KIND_ENV2, APP_SLUG_RE2;
|
|
2221
|
-
var init_dist = __esm(() => {
|
|
2222
|
-
KIND_ENV2 = {
|
|
2223
|
-
config: "HASNA_CONFIG_HOME",
|
|
2224
|
-
data: "HASNA_DATA_HOME",
|
|
2225
|
-
state: "HASNA_STATE_HOME",
|
|
2226
|
-
cache: "HASNA_CACHE_HOME"
|
|
2227
|
-
};
|
|
2228
|
-
APP_SLUG_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2229
|
-
});
|
|
2230
|
-
|
|
2231
|
-
// src/lib/app-home.ts
|
|
2232
|
-
import { existsSync as existsSync3 } from "fs";
|
|
2233
|
-
import { homedir as homedir5 } from "os";
|
|
2234
|
-
import { join as join6, resolve as resolve2 } from "path";
|
|
2235
2286
|
function homeDir(env = process.env) {
|
|
2236
|
-
return env["HOME"] || env["USERPROFILE"] ||
|
|
2287
|
+
return env["HOME"] || env["USERPROFILE"] || homedir2();
|
|
2237
2288
|
}
|
|
2238
2289
|
function legacyStoreHome(env = process.env) {
|
|
2239
|
-
return resolve2(
|
|
2290
|
+
return resolve2(join3(homeDir(env), ".hasna", "instructions"));
|
|
2240
2291
|
}
|
|
2241
2292
|
function resolverStoreHome(env = process.env) {
|
|
2242
|
-
return configDir({ app: "configs", env, home: env.HOME || env.USERPROFILE ||
|
|
2293
|
+
return configDir({ app: "configs", env, home: env.HOME || env.USERPROFILE || homedir2() });
|
|
2243
2294
|
}
|
|
2244
2295
|
function adoptResolverStoreHome(resolved, env = process.env) {
|
|
2245
2296
|
const override = env.HASNA_CONFIG_HOME;
|
|
2246
2297
|
if (typeof override === "string" && override.trim().length > 0)
|
|
2247
2298
|
return true;
|
|
2248
|
-
return existsSync3(
|
|
2299
|
+
return existsSync3(join3(resolved, "instructions.db"));
|
|
2249
2300
|
}
|
|
2250
2301
|
function exactStoreHome(env = process.env) {
|
|
2251
2302
|
const v = env[HASNA_CONFIGS_HOME_ENV];
|
|
@@ -2258,9 +2309,15 @@ function getConfigsStoreHome(env = process.env) {
|
|
|
2258
2309
|
const resolved = resolverStoreHome(env);
|
|
2259
2310
|
return adoptResolverStoreHome(resolved, env) ? resolve2(resolved) : legacyStoreHome(env);
|
|
2260
2311
|
}
|
|
2261
|
-
var HASNA_CONFIGS_HOME_ENV = "HASNA_CONFIGS_HOME";
|
|
2312
|
+
var PATHS_RESOLVER_KIND_ENV2, PATHS_RESOLVER_APP_SLUG_RE2, HASNA_CONFIGS_HOME_ENV = "HASNA_CONFIGS_HOME";
|
|
2262
2313
|
var init_app_home = __esm(() => {
|
|
2263
|
-
|
|
2314
|
+
PATHS_RESOLVER_KIND_ENV2 = {
|
|
2315
|
+
config: "HASNA_CONFIG_HOME",
|
|
2316
|
+
data: "HASNA_DATA_HOME",
|
|
2317
|
+
state: "HASNA_STATE_HOME",
|
|
2318
|
+
cache: "HASNA_CACHE_HOME"
|
|
2319
|
+
};
|
|
2320
|
+
PATHS_RESOLVER_APP_SLUG_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2264
2321
|
});
|
|
2265
2322
|
|
|
2266
2323
|
// src/lib/raw-store-root.ts
|
|
@@ -2275,7 +2332,7 @@ var init_raw_store_root = __esm(() => {
|
|
|
2275
2332
|
// src/db/database.ts
|
|
2276
2333
|
import { Database } from "bun:sqlite";
|
|
2277
2334
|
import { existsSync as existsSync5, mkdirSync, rmSync } from "fs";
|
|
2278
|
-
import { join as
|
|
2335
|
+
import { join as join5 } from "path";
|
|
2279
2336
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2280
2337
|
function getDbPath() {
|
|
2281
2338
|
if (process.env["HASNA_INSTRUCTIONS_DB_PATH"]) {
|
|
@@ -2283,7 +2340,7 @@ function getDbPath() {
|
|
|
2283
2340
|
}
|
|
2284
2341
|
const dir = getRawStoreRoot();
|
|
2285
2342
|
mkdirSync(dir, { recursive: true });
|
|
2286
|
-
return
|
|
2343
|
+
return join5(dir, "instructions.db");
|
|
2287
2344
|
}
|
|
2288
2345
|
function uuid() {
|
|
2289
2346
|
return randomUUID3();
|
|
@@ -2297,9 +2354,8 @@ function slugify(name) {
|
|
|
2297
2354
|
function getDatabase(path) {
|
|
2298
2355
|
if (_db)
|
|
2299
2356
|
return _db;
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
throw new Error("instructions is using the HTTP API transport (HASNA_INSTRUCTIONS_API_URL set): this command is not wired to the API yet. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to use it against the local store.");
|
|
2357
|
+
if (!path && hasInstructionsEnvAuthorityIntent(process.env)) {
|
|
2358
|
+
throw new Error("instructions is using the hosted API transport (a HASNA_INSTRUCTIONS_* credential is configured): this command is not wired to the API yet. " + "Point this run at the local store (HASNA_INSTRUCTIONS_LOCAL=1 with no hosted credential) to use it against the local SQLite store.");
|
|
2303
2359
|
}
|
|
2304
2360
|
const dbPath = path || getDbPath();
|
|
2305
2361
|
const db = new Database(dbPath);
|
|
@@ -2380,7 +2436,7 @@ function insertFeedback(input, db) {
|
|
|
2380
2436
|
}
|
|
2381
2437
|
var MIGRATIONS, _db = null;
|
|
2382
2438
|
var init_database = __esm(() => {
|
|
2383
|
-
|
|
2439
|
+
init_local_opt_in();
|
|
2384
2440
|
init_raw_store_root();
|
|
2385
2441
|
MIGRATIONS = [
|
|
2386
2442
|
`
|
|
@@ -2761,9 +2817,9 @@ var init_template = __esm(() => {
|
|
|
2761
2817
|
});
|
|
2762
2818
|
|
|
2763
2819
|
// src/lib/machine.ts
|
|
2764
|
-
import { arch as currentArch, homedir as
|
|
2820
|
+
import { arch as currentArch, homedir as homedir3, hostname as currentHostname, type as currentOsType } from "os";
|
|
2765
2821
|
import { existsSync as existsSync6 } from "fs";
|
|
2766
|
-
import { join as
|
|
2822
|
+
import { join as join6 } from "path";
|
|
2767
2823
|
function normalizeOsFamily(os) {
|
|
2768
2824
|
const value = (os ?? "").trim().toLowerCase();
|
|
2769
2825
|
if (value === "darwin" || value === "macos" || value === "mac" || value === "osx")
|
|
@@ -2775,11 +2831,11 @@ function normalizeOsFamily(os) {
|
|
|
2775
2831
|
return value || "unknown";
|
|
2776
2832
|
}
|
|
2777
2833
|
function detectMachineContext(overrides = {}) {
|
|
2778
|
-
const homeDir2 = overrides.home_dir ?? process.env["CONFIGS_HOME"] ?? process.env["HOME"] ??
|
|
2834
|
+
const homeDir2 = overrides.home_dir ?? process.env["CONFIGS_HOME"] ?? process.env["HOME"] ?? homedir3();
|
|
2779
2835
|
const os = overrides.os ?? currentOsType();
|
|
2780
2836
|
const osFamily = normalizeOsFamily(os);
|
|
2781
|
-
const bunBinDir = overrides.bun_bin_dir ??
|
|
2782
|
-
const defaultBunPath = osFamily === "macos" && existsSync6(BREW_BUN_PATH) ? BREW_BUN_PATH :
|
|
2837
|
+
const bunBinDir = overrides.bun_bin_dir ?? join6(homeDir2, ".bun", "bin");
|
|
2838
|
+
const defaultBunPath = osFamily === "macos" && existsSync6(BREW_BUN_PATH) ? BREW_BUN_PATH : join6(bunBinDir, "bun");
|
|
2783
2839
|
return {
|
|
2784
2840
|
id: "current-machine",
|
|
2785
2841
|
hostname: overrides.hostname ?? currentHostname(),
|
|
@@ -2789,10 +2845,10 @@ function detectMachineContext(overrides = {}) {
|
|
|
2789
2845
|
created_at: "",
|
|
2790
2846
|
os_family: osFamily,
|
|
2791
2847
|
home_dir: homeDir2,
|
|
2792
|
-
workspace_root: overrides.workspace_root ??
|
|
2848
|
+
workspace_root: overrides.workspace_root ?? join6(homeDir2, osFamily === "macos" ? "Workspace" : "workspace"),
|
|
2793
2849
|
bun_bin_dir: bunBinDir,
|
|
2794
2850
|
bun_path: overrides.bun_path ?? defaultBunPath,
|
|
2795
|
-
path_prefix: overrides.path_prefix ?? (osFamily === "macos" ? `${
|
|
2851
|
+
path_prefix: overrides.path_prefix ?? (osFamily === "macos" ? `${join6("/opt", "homebrew", "bin")}:${bunBinDir}` : bunBinDir)
|
|
2796
2852
|
};
|
|
2797
2853
|
}
|
|
2798
2854
|
function machineContextToVariables(machine) {
|
|
@@ -7420,6 +7476,114 @@ var init_session_render_contract = __esm(() => {
|
|
|
7420
7476
|
];
|
|
7421
7477
|
});
|
|
7422
7478
|
|
|
7479
|
+
// src/lib/session-render-state.ts
|
|
7480
|
+
import { existsSync as existsSync7, readdirSync } from "fs";
|
|
7481
|
+
import { homedir as homedir4 } from "os";
|
|
7482
|
+
import { dirname, join as join7, resolve as resolve4 } from "path";
|
|
7483
|
+
import { homedir as pathsResolverHomedir3 } from "os";
|
|
7484
|
+
import { join as pathsResolverJoin3 } from "path";
|
|
7485
|
+
function pathsResolverAssertApp3(app) {
|
|
7486
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
7487
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
7488
|
+
}
|
|
7489
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE3.test(app)) {
|
|
7490
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
function pathsResolverAssertKind3(kind) {
|
|
7494
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV3).includes(kind)) {
|
|
7495
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV3).join(", ")}`);
|
|
7496
|
+
}
|
|
7497
|
+
}
|
|
7498
|
+
function pathsResolverBaseDir3(kind, options) {
|
|
7499
|
+
pathsResolverAssertKind3(kind);
|
|
7500
|
+
const env = options.env ?? process.env;
|
|
7501
|
+
const override = env[PATHS_RESOLVER_KIND_ENV3[kind]];
|
|
7502
|
+
if (typeof override === "string" && override.length > 0)
|
|
7503
|
+
return override;
|
|
7504
|
+
const home = options.home ?? pathsResolverHomedir3();
|
|
7505
|
+
const platform = options.platform ?? process.platform;
|
|
7506
|
+
if (platform === "darwin") {
|
|
7507
|
+
switch (kind) {
|
|
7508
|
+
case "config":
|
|
7509
|
+
case "data":
|
|
7510
|
+
return pathsResolverJoin3(home, "Library", "Application Support", "Hasna");
|
|
7511
|
+
case "cache":
|
|
7512
|
+
return pathsResolverJoin3(home, "Library", "Caches", "Hasna");
|
|
7513
|
+
case "state":
|
|
7514
|
+
return pathsResolverJoin3(home, "Library", "Logs", "Hasna");
|
|
7515
|
+
}
|
|
7516
|
+
}
|
|
7517
|
+
switch (kind) {
|
|
7518
|
+
case "config":
|
|
7519
|
+
return pathsResolverJoin3(home, ".config", "hasna");
|
|
7520
|
+
case "data":
|
|
7521
|
+
return pathsResolverJoin3(home, ".local", "share", "hasna");
|
|
7522
|
+
case "state":
|
|
7523
|
+
return pathsResolverJoin3(home, ".local", "state", "hasna");
|
|
7524
|
+
case "cache":
|
|
7525
|
+
return pathsResolverJoin3(home, ".cache", "hasna");
|
|
7526
|
+
}
|
|
7527
|
+
}
|
|
7528
|
+
function pathsResolverResolve3(kind, options) {
|
|
7529
|
+
pathsResolverAssertApp3(options.app);
|
|
7530
|
+
const appSegment = options.internal === true ? pathsResolverJoin3("internal", options.app) : options.app;
|
|
7531
|
+
return pathsResolverJoin3(pathsResolverBaseDir3(kind, options), appSegment);
|
|
7532
|
+
}
|
|
7533
|
+
function stateDir(options) {
|
|
7534
|
+
return pathsResolverResolve3("state", options);
|
|
7535
|
+
}
|
|
7536
|
+
function homeDir2(env = process.env) {
|
|
7537
|
+
return env["HOME"] || env["USERPROFILE"] || homedir4();
|
|
7538
|
+
}
|
|
7539
|
+
function legacySnapshotDir(targetHome) {
|
|
7540
|
+
return resolve4(join7(targetHome, ".hasna", "session-render-snapshots"));
|
|
7541
|
+
}
|
|
7542
|
+
function resolverSnapshotDir(env = process.env) {
|
|
7543
|
+
const override = env.HASNA_STATE_HOME;
|
|
7544
|
+
if (typeof override === "string" && override.trim().length > 0 && !existsSync7(override)) {
|
|
7545
|
+
return stateDir({
|
|
7546
|
+
app: SESSION_RENDER_STATE_APP,
|
|
7547
|
+
env: { ...env, HASNA_STATE_HOME: undefined },
|
|
7548
|
+
home: homeDir2(env)
|
|
7549
|
+
});
|
|
7550
|
+
}
|
|
7551
|
+
return stateDir({ app: SESSION_RENDER_STATE_APP, env, home: homeDir2(env) });
|
|
7552
|
+
}
|
|
7553
|
+
function adoptResolverSnapshotDir(resolved, env = process.env) {
|
|
7554
|
+
const override = env.HASNA_STATE_HOME;
|
|
7555
|
+
if (typeof override === "string" && override.trim().length > 0 && existsSync7(override)) {
|
|
7556
|
+
return true;
|
|
7557
|
+
}
|
|
7558
|
+
if (!existsSync7(resolved))
|
|
7559
|
+
return false;
|
|
7560
|
+
return readdirSync(resolved).some((name) => name.endsWith(".json"));
|
|
7561
|
+
}
|
|
7562
|
+
function resolveSessionRenderSnapshotLocation(targetHome, env = process.env) {
|
|
7563
|
+
const resolved = resolverSnapshotDir(env);
|
|
7564
|
+
if (!adoptResolverSnapshotDir(resolved, env)) {
|
|
7565
|
+
return { dir: legacySnapshotDir(targetHome), workspaceRoot: targetHome, adopted: false };
|
|
7566
|
+
}
|
|
7567
|
+
const resolvedPath = resolve4(resolved);
|
|
7568
|
+
return { dir: resolvedPath, workspaceRoot: dirname(resolvedPath), adopted: true };
|
|
7569
|
+
}
|
|
7570
|
+
function getSessionRenderSnapshotDir(targetHome, env = process.env) {
|
|
7571
|
+
return resolveSessionRenderSnapshotLocation(targetHome, env).dir;
|
|
7572
|
+
}
|
|
7573
|
+
function sessionRenderSnapshotWorkspaceRoot(targetHome, env = process.env) {
|
|
7574
|
+
return resolveSessionRenderSnapshotLocation(targetHome, env).workspaceRoot;
|
|
7575
|
+
}
|
|
7576
|
+
var PATHS_RESOLVER_KIND_ENV3, PATHS_RESOLVER_APP_SLUG_RE3, SESSION_RENDER_STATE_APP = "instructions";
|
|
7577
|
+
var init_session_render_state = __esm(() => {
|
|
7578
|
+
PATHS_RESOLVER_KIND_ENV3 = {
|
|
7579
|
+
config: "HASNA_CONFIG_HOME",
|
|
7580
|
+
data: "HASNA_DATA_HOME",
|
|
7581
|
+
state: "HASNA_STATE_HOME",
|
|
7582
|
+
cache: "HASNA_CACHE_HOME"
|
|
7583
|
+
};
|
|
7584
|
+
PATHS_RESOLVER_APP_SLUG_RE3 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7585
|
+
});
|
|
7586
|
+
|
|
7423
7587
|
// src/lib/project-context.ts
|
|
7424
7588
|
import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
|
|
7425
7589
|
import { execFileSync } from "child_process";
|
|
@@ -7427,7 +7591,7 @@ import { dlopen, FFIType } from "bun:ffi";
|
|
|
7427
7591
|
import {
|
|
7428
7592
|
closeSync,
|
|
7429
7593
|
constants,
|
|
7430
|
-
existsSync as
|
|
7594
|
+
existsSync as existsSync8,
|
|
7431
7595
|
fstatSync,
|
|
7432
7596
|
fsyncSync,
|
|
7433
7597
|
lstatSync,
|
|
@@ -7440,7 +7604,7 @@ import {
|
|
|
7440
7604
|
statSync,
|
|
7441
7605
|
writeFileSync
|
|
7442
7606
|
} from "fs";
|
|
7443
|
-
import { basename, dirname, isAbsolute, join as
|
|
7607
|
+
import { basename, dirname as dirname2, isAbsolute, join as join8, parse, relative, resolve as resolve5 } from "path";
|
|
7444
7608
|
function managedObservationMaxBytes(relativePath) {
|
|
7445
7609
|
return SESSION_MANAGED_OUTPUT_PATHS.includes(relativePath) ? SESSION_MANAGED_OUTPUT_MAX_BYTES : FOREIGN_INPUT_MAX_BYTES;
|
|
7446
7610
|
}
|
|
@@ -7520,7 +7684,7 @@ function planProjectContext(input) {
|
|
|
7520
7684
|
const inlineMarkerOverhead = nativeImports ? 0 : Buffer.byteLength(buildManagedBlock(bundle, "", `
|
|
7521
7685
|
`), "utf8");
|
|
7522
7686
|
const generated = buildCanonicalFragment(bundle, status, ageSeconds, PROJECT_CONTEXT_MAX_RENDERED_BYTES - Math.max(320, inlineMarkerOverhead), PROJECT_CONTEXT_MAX_APPROX_TOKENS - Math.max(80, Math.ceil(inlineMarkerOverhead / 4)));
|
|
7523
|
-
const previousTargetContent =
|
|
7687
|
+
const previousTargetContent = existsSync8(paths.target) ? readUtf8RegularFile(paths.target, workspaceRoot, managedObservationMaxBytes(relativePosix(workspaceRoot, paths.target))) : null;
|
|
7524
7688
|
const markerParse = parseManagedBlock(previousTargetContent ?? "", input.force === true);
|
|
7525
7689
|
if (markerParse.block && markerParse.block.id !== bundle.project.id) {
|
|
7526
7690
|
throw new ProjectContextError("MANAGED_BLOCK_CONFLICT", "managed block belongs to a different project");
|
|
@@ -7571,7 +7735,7 @@ function composeProjectContextSessionRender(input) {
|
|
|
7571
7735
|
return null;
|
|
7572
7736
|
const { runtime, workspace_root: workspaceRoot, observed_hashes: observedHashes } = guard;
|
|
7573
7737
|
const paths = runtimePaths(workspaceRoot, runtime);
|
|
7574
|
-
if (!
|
|
7738
|
+
if (!existsSync8(paths.manifest))
|
|
7575
7739
|
return null;
|
|
7576
7740
|
assertCodewithTargetIsConsumed(workspaceRoot, runtime);
|
|
7577
7741
|
const manifest = readProjectContextManifest(paths.manifest, workspaceRoot);
|
|
@@ -7600,7 +7764,7 @@ function composeProjectContextSessionRender(input) {
|
|
|
7600
7764
|
}
|
|
7601
7765
|
const fragment = readUtf8RegularFile(paths.fragment, workspaceRoot, PROJECT_CONTEXT_MAX_RENDERED_BYTES);
|
|
7602
7766
|
scanGeneratedContent(fragment);
|
|
7603
|
-
if (!
|
|
7767
|
+
if (!existsSync8(paths.target)) {
|
|
7604
7768
|
throw new ProjectContextError("MANAGED_BLOCK_CONFLICT", "project-context provider target is missing while durable context is active");
|
|
7605
7769
|
}
|
|
7606
7770
|
const currentTarget = readUtf8RegularFile(paths.target, workspaceRoot, managedObservationMaxBytes(relativePosix(workspaceRoot, paths.target)));
|
|
@@ -7611,7 +7775,7 @@ function composeProjectContextSessionRender(input) {
|
|
|
7611
7775
|
if (currentMarkers.block.id !== cache.project_id || currentMarkers.block.revision !== cache.revision || currentMarkers.block.hash !== cache.hash) {
|
|
7612
7776
|
throw new ProjectContextError("MANAGED_BLOCK_CONFLICT", "project-context provider markers differ from the durable cache");
|
|
7613
7777
|
}
|
|
7614
|
-
const plannedIndexes = input.files.filter((file) => file.role === "index" &&
|
|
7778
|
+
const plannedIndexes = input.files.filter((file) => file.role === "index" && resolve5(file.path) === paths.target);
|
|
7615
7779
|
if (plannedIndexes.length !== 1) {
|
|
7616
7780
|
throw new ProjectContextError("PROJECT_CONTEXT_MANIFEST_INVALID", "session renderer does not own the selected project-context provider target");
|
|
7617
7781
|
}
|
|
@@ -7669,7 +7833,7 @@ function withProjectContextSessionGuard(guard, action, options = {}) {
|
|
|
7669
7833
|
verify();
|
|
7670
7834
|
return action(null);
|
|
7671
7835
|
}
|
|
7672
|
-
const lockPath =
|
|
7836
|
+
const lockPath = resolve5(validated.workspace_root, ...PROJECT_CONTEXT_LOCK_PATH.split("/"));
|
|
7673
7837
|
const lock = acquireWorkspaceLock(validated.workspace_root, lockPath);
|
|
7674
7838
|
try {
|
|
7675
7839
|
verify();
|
|
@@ -7695,7 +7859,7 @@ function validateProjectContextSessionGuard(guard) {
|
|
|
7695
7859
|
if (!isRecord(observed) || typeof observed.path !== "string") {
|
|
7696
7860
|
throw new ProjectContextError("PROJECT_CONTEXT_SESSION_STALE", "session project-context guard contains malformed hash metadata");
|
|
7697
7861
|
}
|
|
7698
|
-
const path =
|
|
7862
|
+
const path = resolve5(observed.path);
|
|
7699
7863
|
if (!allowedPaths.has(path) || observedPaths.has(path)) {
|
|
7700
7864
|
throw new ProjectContextError("PROJECT_CONTEXT_SESSION_STALE", "session project-context guard contains an unexpected or duplicate path");
|
|
7701
7865
|
}
|
|
@@ -7717,7 +7881,7 @@ function validateProjectContextSessionGuard(guard) {
|
|
|
7717
7881
|
function applyProjectContext(options) {
|
|
7718
7882
|
const workspaceRoot = assertSafeWorkspaceRoot(options.workspace_root);
|
|
7719
7883
|
const now3 = options.now ?? new Date;
|
|
7720
|
-
const lockPath =
|
|
7884
|
+
const lockPath = resolve5(workspaceRoot, ...PROJECT_CONTEXT_LOCK_PATH.split("/"));
|
|
7721
7885
|
const lock = options.dry_run ? null : acquireWorkspaceLock(workspaceRoot, lockPath, options.test_hooks?.after_lock_open, options.test_hooks?.before_stale_lock_remove, options.test_hooks?.process_start_identity);
|
|
7722
7886
|
try {
|
|
7723
7887
|
const resolved = resolveBundleForApply(options, workspaceRoot, now3);
|
|
@@ -7864,7 +8028,7 @@ function resolveBundleForApply(options, workspaceRoot, now3) {
|
|
|
7864
8028
|
if (!options.expected_project_id) {
|
|
7865
8029
|
throw new ProjectContextError("PROJECT_CONTEXT_CACHE_ID_REQUIRED", "expected_project_id is required for stale-cache fallback");
|
|
7866
8030
|
}
|
|
7867
|
-
const cachePath =
|
|
8031
|
+
const cachePath = resolve5(workspaceRoot, ...PROJECT_CONTEXT_CACHE_PATH.split("/"));
|
|
7868
8032
|
const cache = readProjectContextCache(cachePath, workspaceRoot);
|
|
7869
8033
|
if (!cache)
|
|
7870
8034
|
throw new ProjectContextError("PROJECT_CONTEXT_CACHE_MISSING", "no last-known-good project context cache exists");
|
|
@@ -8066,7 +8230,7 @@ function findLegacyCodewithWorkspaceSection(workspaceRoot, runtime, content, bun
|
|
|
8066
8230
|
if (runtime !== "codewith" || !content)
|
|
8067
8231
|
return null;
|
|
8068
8232
|
const sessionManifestPath = runtimePaths(workspaceRoot, runtime).sessionManifest;
|
|
8069
|
-
if (!
|
|
8233
|
+
if (!existsSync8(sessionManifestPath))
|
|
8070
8234
|
return null;
|
|
8071
8235
|
const manifest = readSessionManifestRecord(sessionManifestPath, workspaceRoot);
|
|
8072
8236
|
if (!manifest || manifest["schema"] !== SESSION_RENDER_SCHEMA) {
|
|
@@ -8117,7 +8281,7 @@ function assertRevisionOrdering(plan, force) {
|
|
|
8117
8281
|
const manifest = readProjectContextManifest(plan.manifest_path, plan.workspace_root);
|
|
8118
8282
|
if (manifest) {
|
|
8119
8283
|
const manifestHashHasRecoveryProof = manifest.projectContext.hash === plan.bundle.hash || metadataSnapshotMatchesManifest(plan, manifest);
|
|
8120
|
-
const canonicalStateAlreadyInstalled = cache !== null && canonicalCacheHash === plan.bundle.hash && cache.project_id === plan.bundle.project.id && cache.revision === plan.bundle.revision && plan.marker !== null && plan.marker.id === plan.bundle.project.id && plan.marker.revision === plan.bundle.revision && plan.marker.hash === plan.bundle.hash &&
|
|
8284
|
+
const canonicalStateAlreadyInstalled = cache !== null && canonicalCacheHash === plan.bundle.hash && cache.project_id === plan.bundle.project.id && cache.revision === plan.bundle.revision && plan.marker !== null && plan.marker.id === plan.bundle.project.id && plan.marker.revision === plan.bundle.revision && plan.marker.hash === plan.bundle.hash && existsSync8(plan.fragment_path) && fragmentMatchesBundle(plan.fragment_path, plan.bundle, plan.workspace_root) && manifestHashHasRecoveryProof;
|
|
8121
8285
|
observations.push({
|
|
8122
8286
|
source: "manifest",
|
|
8123
8287
|
id: manifest.projectContext.projectId,
|
|
@@ -8125,7 +8289,7 @@ function assertRevisionOrdering(plan, force) {
|
|
|
8125
8289
|
hash: canonicalStateAlreadyInstalled && manifest.projectContext.projectId === plan.bundle.project.id && manifest.projectContext.revision === plan.bundle.revision ? plan.bundle.hash : normalizePersistedHash(manifest.projectContext.revision, manifest.projectContext.hash)
|
|
8126
8290
|
});
|
|
8127
8291
|
const fragmentEntry = manifest.files.find((file) => file.relativePath === PROJECT_CONTEXT_FRAGMENT_PATH);
|
|
8128
|
-
if (fragmentEntry &&
|
|
8292
|
+
if (fragmentEntry && existsSync8(plan.fragment_path)) {
|
|
8129
8293
|
const actual = currentFileHash(plan.fragment_path, plan.workspace_root);
|
|
8130
8294
|
if (actual !== fragmentEntry.sha256 && !fragmentMatchesBundle(plan.fragment_path, plan.bundle, plan.workspace_root) && !force) {
|
|
8131
8295
|
throw new ProjectContextError("MANAGED_BLOCK_CONFLICT", "canonical project-context fragment changed outside Instructions");
|
|
@@ -8220,9 +8384,9 @@ function buildManifest(plan, now3) {
|
|
|
8220
8384
|
function buildSessionCompatibilityManifest(plan, now3) {
|
|
8221
8385
|
const paths = runtimePaths(plan.workspace_root, plan.runtime);
|
|
8222
8386
|
const tool = manifestTool(plan.runtime);
|
|
8223
|
-
const targetHome = plan.runtime === "codewith" ?
|
|
8387
|
+
const targetHome = plan.runtime === "codewith" ? resolve5(plan.workspace_root, ".codewith") : plan.workspace_root;
|
|
8224
8388
|
const targetRelativePath = sessionTargetRelativePath(plan.runtime);
|
|
8225
|
-
const existing =
|
|
8389
|
+
const existing = existsSync8(paths.sessionManifest) ? readSessionManifestRecord(paths.sessionManifest, plan.workspace_root) : {
|
|
8226
8390
|
schema: SESSION_RENDER_SCHEMA,
|
|
8227
8391
|
tool,
|
|
8228
8392
|
adapterMode: plan.native_imports ? "native-imports" : "flattened-markdown",
|
|
@@ -8242,7 +8406,7 @@ function buildSessionCompatibilityManifest(plan, now3) {
|
|
|
8242
8406
|
throw new ProjectContextError("PROJECT_CONTEXT_MANIFEST_INVALID", "provider session manifest is malformed or incompatible");
|
|
8243
8407
|
}
|
|
8244
8408
|
const existingTargetHome = safeLegacyMetadataString(existing["targetHome"], null);
|
|
8245
|
-
if (existingTargetHome !== null &&
|
|
8409
|
+
if (existingTargetHome !== null && resolve5(existingTargetHome) !== targetHome) {
|
|
8246
8410
|
throw new ProjectContextError("PROJECT_CONTEXT_MANIFEST_INVALID", "provider session manifest targets a different workspace");
|
|
8247
8411
|
}
|
|
8248
8412
|
const sources = sanitizeLegacySources(existing["sources"]).filter((source) => source["id"] !== "project-context-bundle");
|
|
@@ -8528,9 +8692,9 @@ function writeMetadataSnapshot(plan, now3) {
|
|
|
8528
8692
|
const previous = readProjectContextManifest(plan.manifest_path, plan.workspace_root);
|
|
8529
8693
|
if (!previous || previous.projectContext.revision === plan.bundle.revision && previous.projectContext.hash === plan.bundle.hash)
|
|
8530
8694
|
return null;
|
|
8531
|
-
const snapshotDir =
|
|
8695
|
+
const snapshotDir = resolve5(plan.workspace_root, ...PROJECT_CONTEXT_SNAPSHOT_DIR.split("/"));
|
|
8532
8696
|
ensureSafeDirectory(snapshotDir, plan.workspace_root, 448);
|
|
8533
|
-
const snapshotPath =
|
|
8697
|
+
const snapshotPath = resolve5(snapshotDir, `${safeFilename(previous.projectContext.revision)}-${previous.projectContext.hash.slice(-12)}.json`);
|
|
8534
8698
|
const snapshot = {
|
|
8535
8699
|
schema: "hasna.configs.session-render-snapshot/v1",
|
|
8536
8700
|
kind: "project-context-metadata",
|
|
@@ -8546,9 +8710,9 @@ function writeMetadataSnapshot(plan, now3) {
|
|
|
8546
8710
|
return snapshotPath;
|
|
8547
8711
|
}
|
|
8548
8712
|
function metadataSnapshotMatchesManifest(plan, manifest) {
|
|
8549
|
-
const snapshotDir =
|
|
8550
|
-
const snapshotPath =
|
|
8551
|
-
if (!
|
|
8713
|
+
const snapshotDir = resolve5(plan.workspace_root, ...PROJECT_CONTEXT_SNAPSHOT_DIR.split("/"));
|
|
8714
|
+
const snapshotPath = resolve5(snapshotDir, `${safeFilename(manifest.projectContext.revision)}-${manifest.projectContext.hash.slice(-12)}.json`);
|
|
8715
|
+
if (!existsSync8(snapshotPath))
|
|
8552
8716
|
return false;
|
|
8553
8717
|
const record = readJsonRecord(snapshotPath, plan.workspace_root);
|
|
8554
8718
|
const result = projectContextMetadataSnapshotSchema.safeParse(record);
|
|
@@ -8596,10 +8760,11 @@ function writeProjectContextRollbackSnapshot(plan, now3, outputs) {
|
|
|
8596
8760
|
sha256: nextHash
|
|
8597
8761
|
};
|
|
8598
8762
|
});
|
|
8599
|
-
const
|
|
8600
|
-
|
|
8763
|
+
const snapshotWorkspaceRoot = sessionRenderSnapshotWorkspaceRoot(plan.workspace_root);
|
|
8764
|
+
const snapshotDir = getSessionRenderSnapshotDir(plan.workspace_root);
|
|
8765
|
+
ensureSafeDirectory(snapshotDir, snapshotWorkspaceRoot, 448);
|
|
8601
8766
|
const timestamp = now3.toISOString().replace(/[:.]/g, "-");
|
|
8602
|
-
const snapshotPath =
|
|
8767
|
+
const snapshotPath = resolve5(snapshotDir, `${timestamp}-${randomUUID5()}.json`);
|
|
8603
8768
|
const snapshot = {
|
|
8604
8769
|
schema: "hasna.configs.session-render-snapshot/v2",
|
|
8605
8770
|
createdAt: now3.toISOString(),
|
|
@@ -8613,11 +8778,11 @@ function writeProjectContextRollbackSnapshot(plan, now3, outputs) {
|
|
|
8613
8778
|
afterFiles
|
|
8614
8779
|
};
|
|
8615
8780
|
atomicWriteFile(snapshotPath, `${JSON.stringify(snapshot, null, 2)}
|
|
8616
|
-
`,
|
|
8781
|
+
`, snapshotWorkspaceRoot, 384, null);
|
|
8617
8782
|
return snapshotPath;
|
|
8618
8783
|
}
|
|
8619
8784
|
function readProjectContextManifest(path, workspaceRoot) {
|
|
8620
|
-
if (!
|
|
8785
|
+
if (!existsSync8(path))
|
|
8621
8786
|
return null;
|
|
8622
8787
|
const record = readJsonRecord(path, workspaceRoot);
|
|
8623
8788
|
const result = storedManifestObservationSchema.safeParse(record);
|
|
@@ -8632,7 +8797,7 @@ function readProjectContextManifest(path, workspaceRoot) {
|
|
|
8632
8797
|
};
|
|
8633
8798
|
}
|
|
8634
8799
|
function readProjectContextCache(path, workspaceRoot) {
|
|
8635
|
-
if (!
|
|
8800
|
+
if (!existsSync8(path))
|
|
8636
8801
|
return null;
|
|
8637
8802
|
const record = readJsonRecord(path, workspaceRoot);
|
|
8638
8803
|
const result = projectContextCacheSchema.safeParse(record);
|
|
@@ -8664,7 +8829,7 @@ function readSessionManifestRecord(path, workspaceRoot) {
|
|
|
8664
8829
|
}
|
|
8665
8830
|
}
|
|
8666
8831
|
function atomicWriteFile(path, content, workspaceRoot, defaultMode, expectedHash, afterExchange, atomicExchangeUnavailable = false, beforeInstall, portableCreateOnly = false, maxObservedBytes, allowPortableReplacement = false) {
|
|
8667
|
-
const dir =
|
|
8832
|
+
const dir = resolve5(path, "..");
|
|
8668
8833
|
ensureSafeDirectory(dir, workspaceRoot, 448);
|
|
8669
8834
|
assertNoSymlinkSegments(workspaceRoot, path);
|
|
8670
8835
|
const anchoredOps = portableCreateOnly ? null : resolveAnchoredFsOps();
|
|
@@ -8681,7 +8846,7 @@ function atomicWriteFile(path, content, workspaceRoot, defaultMode, expectedHash
|
|
|
8681
8846
|
const previous = anchoredFileObservation(directory, targetName);
|
|
8682
8847
|
const previousMode = previous?.mode ?? defaultMode;
|
|
8683
8848
|
const tempName = `.project-context-${randomUUID5()}.tmp`;
|
|
8684
|
-
const tempPath =
|
|
8849
|
+
const tempPath = join8(dir, tempName);
|
|
8685
8850
|
let fd = null;
|
|
8686
8851
|
let preserveTemp = false;
|
|
8687
8852
|
let directoryChanged = false;
|
|
@@ -8810,9 +8975,9 @@ function atomicWritePortable(path, content, workspaceRoot, defaultMode, expected
|
|
|
8810
8975
|
if (currentHash !== null) {
|
|
8811
8976
|
throw new ProjectContextHashRace(`managed path appeared before portable creation: ${relativePosix(workspaceRoot, path)}`);
|
|
8812
8977
|
}
|
|
8813
|
-
const dir =
|
|
8978
|
+
const dir = dirname2(path);
|
|
8814
8979
|
const directoryIdentity = captureManagedDirectoryIdentity(dir, workspaceRoot);
|
|
8815
|
-
const tempPath =
|
|
8980
|
+
const tempPath = join8(dir, `.project-context-${randomUUID5()}.tmp`);
|
|
8816
8981
|
let fd = null;
|
|
8817
8982
|
let tempIdentity = null;
|
|
8818
8983
|
try {
|
|
@@ -8867,9 +9032,9 @@ function atomicWritePortableReplacement(path, content, workspaceRoot, expectedHa
|
|
|
8867
9032
|
if (current.isSymbolicLink() || !current.isFile()) {
|
|
8868
9033
|
throw new ProjectContextHashRace(`managed path is not a regular file: ${relativePosix(workspaceRoot, path)}`);
|
|
8869
9034
|
}
|
|
8870
|
-
const dir =
|
|
9035
|
+
const dir = dirname2(path);
|
|
8871
9036
|
const directoryIdentity = captureManagedDirectoryIdentity(dir, workspaceRoot);
|
|
8872
|
-
const tempPath =
|
|
9037
|
+
const tempPath = join8(dir, `.project-context-${randomUUID5()}.tmp`);
|
|
8873
9038
|
const desiredHash = sha2562(content);
|
|
8874
9039
|
let fd = null;
|
|
8875
9040
|
let tempIdentity = null;
|
|
@@ -8926,7 +9091,7 @@ function portablePreparedHash(tempPath, path, workspaceRoot, maxObservedBytes, s
|
|
|
8926
9091
|
function portableFileHash(path, workspaceRoot, maxObservedBytes) {
|
|
8927
9092
|
if (maxObservedBytes === undefined)
|
|
8928
9093
|
return currentFileHash(path, workspaceRoot);
|
|
8929
|
-
if (!
|
|
9094
|
+
if (!existsSync8(path))
|
|
8930
9095
|
return null;
|
|
8931
9096
|
assertNoSymlinkSegments(workspaceRoot, path);
|
|
8932
9097
|
const stat = lstatSync(path);
|
|
@@ -8938,13 +9103,13 @@ function portableFileHash(path, workspaceRoot, maxObservedBytes) {
|
|
|
8938
9103
|
return createHash2("sha256").update(readFileSync(path)).digest("hex");
|
|
8939
9104
|
}
|
|
8940
9105
|
function writeProjectContextCoordinatedFile(input) {
|
|
8941
|
-
atomicWriteFile(
|
|
9106
|
+
atomicWriteFile(resolve5(input.path), input.content, assertSafeWorkspaceRoot(input.workspace_root), input.default_mode ?? 420, input.expected_hash, undefined, false, input.test_hooks?.before_install, input.force_portable_file_ops ?? false, input.max_observed_bytes, input.allow_portable_replacement ?? false);
|
|
8942
9107
|
}
|
|
8943
9108
|
function removeProjectContextCoordinatedFile(input) {
|
|
8944
9109
|
const workspaceRoot = assertSafeWorkspaceRoot(input.workspace_root);
|
|
8945
|
-
const path =
|
|
9110
|
+
const path = resolve5(input.path);
|
|
8946
9111
|
assertNoSymlinkSegments(workspaceRoot, path);
|
|
8947
|
-
const dir =
|
|
9112
|
+
const dir = dirname2(path);
|
|
8948
9113
|
const anchoredOps = input.force_portable_file_ops ? null : resolveAnchoredFsOps();
|
|
8949
9114
|
if (!anchoredOps) {
|
|
8950
9115
|
if (!input.allow_portable_removal) {
|
|
@@ -8968,7 +9133,7 @@ function removeProjectContextCoordinatedFile(input) {
|
|
|
8968
9133
|
throw new ProjectContextHashRace(`managed path changed during deletion: ${relativePosix(workspaceRoot, path)}`);
|
|
8969
9134
|
}
|
|
8970
9135
|
displaced = true;
|
|
8971
|
-
input.test_hooks?.after_displace?.(
|
|
9136
|
+
input.test_hooks?.after_displace?.(join8(dir, displacedName));
|
|
8972
9137
|
const moved = anchoredFileObservation(directory, displacedName);
|
|
8973
9138
|
if (!moved || moved.dev !== observed.dev || moved.ino !== observed.ino || moved.hash !== input.expected_hash || anchoredFileObservation(directory, targetName) !== null) {
|
|
8974
9139
|
throw new ProjectContextHashRace(`managed path changed during deletion validation: ${relativePosix(workspaceRoot, path)}`);
|
|
@@ -9012,9 +9177,9 @@ function removePortableCoordinatedFile(path, workspaceRoot, expectedHash, maxObs
|
|
|
9012
9177
|
if (observed.isSymbolicLink() || !observed.isFile()) {
|
|
9013
9178
|
throw new ProjectContextHashRace(`managed path is not a regular file: ${relativePosix(workspaceRoot, path)}`);
|
|
9014
9179
|
}
|
|
9015
|
-
const dir =
|
|
9180
|
+
const dir = dirname2(path);
|
|
9016
9181
|
const directoryIdentity = captureManagedDirectoryIdentity(dir, workspaceRoot);
|
|
9017
|
-
const displacedPath =
|
|
9182
|
+
const displacedPath = join8(dir, `.project-context-delete-${randomUUID5()}.tmp`);
|
|
9018
9183
|
let displaced = false;
|
|
9019
9184
|
try {
|
|
9020
9185
|
assertManagedDirectoryStable(dir, workspaceRoot, directoryIdentity);
|
|
@@ -9025,7 +9190,7 @@ function removePortableCoordinatedFile(path, workspaceRoot, expectedHash, maxObs
|
|
|
9025
9190
|
displaced = true;
|
|
9026
9191
|
afterDisplace?.(displacedPath);
|
|
9027
9192
|
const moved = lstatSync(displacedPath);
|
|
9028
|
-
if (moved.isSymbolicLink() || !moved.isFile() || moved.dev !== observed.dev || moved.ino !== observed.ino || portableFileHash(displacedPath, workspaceRoot, maxObservedBytes) !== expectedHash ||
|
|
9193
|
+
if (moved.isSymbolicLink() || !moved.isFile() || moved.dev !== observed.dev || moved.ino !== observed.ino || portableFileHash(displacedPath, workspaceRoot, maxObservedBytes) !== expectedHash || existsSync8(path)) {
|
|
9029
9194
|
throw new ProjectContextHashRace(`managed path changed during portable deletion: ${relativePosix(workspaceRoot, path)}`);
|
|
9030
9195
|
}
|
|
9031
9196
|
rmSync2(displacedPath);
|
|
@@ -9051,7 +9216,7 @@ function restorePortableDisplacedFile(displacedPath, path, workspaceRoot, expect
|
|
|
9051
9216
|
if (displaced.isSymbolicLink() || installed.isSymbolicLink() || !displaced.isFile() || !installed.isFile() || displaced.dev !== expected.dev || displaced.ino !== expected.ino || displaced.dev !== installed.dev || displaced.ino !== installed.ino || portableFileHash(displacedPath, workspaceRoot, maxObservedBytes) !== expectedHash || portableFileHash(path, workspaceRoot, maxObservedBytes) !== expectedHash)
|
|
9052
9217
|
return false;
|
|
9053
9218
|
rmSync2(displacedPath);
|
|
9054
|
-
fsyncDirectory(
|
|
9219
|
+
fsyncDirectory(dirname2(path));
|
|
9055
9220
|
return true;
|
|
9056
9221
|
} catch {
|
|
9057
9222
|
return false;
|
|
@@ -9085,7 +9250,7 @@ function anchoredOpenExclusive(directory, name, mode) {
|
|
|
9085
9250
|
const requestedMode = mode & 4095;
|
|
9086
9251
|
let fd;
|
|
9087
9252
|
try {
|
|
9088
|
-
fd = openSync(
|
|
9253
|
+
fd = openSync(join8(directory.path, name), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, requestedMode);
|
|
9089
9254
|
} catch {
|
|
9090
9255
|
throw new ProjectContextHashRace(`could not create prepared managed file in ${relativePosix(directory.workspaceRoot, directory.path)}`);
|
|
9091
9256
|
}
|
|
@@ -9128,7 +9293,7 @@ function anchoredFileObservation(directory, name) {
|
|
|
9128
9293
|
const stat = fstatSync(fd);
|
|
9129
9294
|
if (!stat.isFile())
|
|
9130
9295
|
throw new ProjectContextHashRace("managed output is not a regular file");
|
|
9131
|
-
const relativePath = relativePosix(directory.workspaceRoot,
|
|
9296
|
+
const relativePath = relativePosix(directory.workspaceRoot, join8(directory.path, name));
|
|
9132
9297
|
const maxBytes = directory.maxObservedBytes === undefined ? managedObservationMaxBytes(relativePath) : directory.maxObservedBytes;
|
|
9133
9298
|
if (maxBytes !== null && stat.size > maxBytes) {
|
|
9134
9299
|
throw new ProjectContextHashRace(`managed output exceeds the safe read limit: ${relativePath}`);
|
|
@@ -9154,7 +9319,7 @@ function anchoredPreparedObservation(directory, name, path, stage) {
|
|
|
9154
9319
|
return observed;
|
|
9155
9320
|
}
|
|
9156
9321
|
function captureManagedDirectoryIdentity(path, workspaceRoot) {
|
|
9157
|
-
assertNoSymlinkSegments(workspaceRoot,
|
|
9322
|
+
assertNoSymlinkSegments(workspaceRoot, join8(path, ".project-context-directory-guard"));
|
|
9158
9323
|
let stat;
|
|
9159
9324
|
try {
|
|
9160
9325
|
stat = lstatSync(path);
|
|
@@ -9167,7 +9332,7 @@ function captureManagedDirectoryIdentity(path, workspaceRoot) {
|
|
|
9167
9332
|
return { dev: stat.dev, ino: stat.ino };
|
|
9168
9333
|
}
|
|
9169
9334
|
function assertManagedDirectoryStable(path, workspaceRoot, expected) {
|
|
9170
|
-
assertNoSymlinkSegments(workspaceRoot,
|
|
9335
|
+
assertNoSymlinkSegments(workspaceRoot, join8(path, ".project-context-directory-guard"));
|
|
9171
9336
|
let current;
|
|
9172
9337
|
try {
|
|
9173
9338
|
current = lstatSync(path);
|
|
@@ -9300,10 +9465,10 @@ function resolveAnchoredFsOps() {
|
|
|
9300
9465
|
return null;
|
|
9301
9466
|
}
|
|
9302
9467
|
function acquireWorkspaceLock(workspaceRoot, lockPath, afterOpen, beforeStaleRemove, processStartIdentityLookup = processStartIdentity) {
|
|
9303
|
-
const lockDirectory =
|
|
9468
|
+
const lockDirectory = resolve5(lockPath, "..");
|
|
9304
9469
|
ensureSafeDirectory(lockDirectory, workspaceRoot, 448);
|
|
9305
9470
|
assertNoSymlinkSegments(workspaceRoot, lockPath);
|
|
9306
|
-
const tempPath =
|
|
9471
|
+
const tempPath = join8(lockDirectory, `.project-context-lock-${randomUUID5()}.tmp`);
|
|
9307
9472
|
let fd = null;
|
|
9308
9473
|
let openedIdentity = null;
|
|
9309
9474
|
let openedContentHash = null;
|
|
@@ -9337,7 +9502,7 @@ function acquireWorkspaceLock(workspaceRoot, lockPath, afterOpen, beforeStaleRem
|
|
|
9337
9502
|
linked = true;
|
|
9338
9503
|
}
|
|
9339
9504
|
fsyncDirectory(lockDirectory);
|
|
9340
|
-
if (
|
|
9505
|
+
if (existsSync8(tempPath)) {
|
|
9341
9506
|
rmSync2(tempPath);
|
|
9342
9507
|
fsyncDirectory(lockDirectory);
|
|
9343
9508
|
}
|
|
@@ -9352,7 +9517,7 @@ function acquireWorkspaceLock(workspaceRoot, lockPath, afterOpen, beforeStaleRem
|
|
|
9352
9517
|
if (linked && openedIdentity && openedContentHash) {
|
|
9353
9518
|
removeOwnedLockByInode(lockPath, openedIdentity, openedContentHash);
|
|
9354
9519
|
}
|
|
9355
|
-
if (!preserveTemp &&
|
|
9520
|
+
if (!preserveTemp && existsSync8(tempPath)) {
|
|
9356
9521
|
try {
|
|
9357
9522
|
rmSync2(tempPath);
|
|
9358
9523
|
} catch {}
|
|
@@ -9367,7 +9532,7 @@ function acquireWorkspaceLock(workspaceRoot, lockPath, afterOpen, beforeStaleRem
|
|
|
9367
9532
|
}
|
|
9368
9533
|
function removeOwnedLockByInode(lockPath, identity, expectedHash) {
|
|
9369
9534
|
try {
|
|
9370
|
-
if (!
|
|
9535
|
+
if (!existsSync8(lockPath))
|
|
9371
9536
|
return;
|
|
9372
9537
|
const current = lstatSync(lockPath);
|
|
9373
9538
|
if (current.isSymbolicLink() || current.dev !== identity.dev || current.ino !== identity.ino)
|
|
@@ -9375,7 +9540,7 @@ function removeOwnedLockByInode(lockPath, identity, expectedHash) {
|
|
|
9375
9540
|
if (expectedHash !== undefined && sha2562(readFileSync(lockPath, "utf8")) !== expectedHash)
|
|
9376
9541
|
return;
|
|
9377
9542
|
rmSync2(lockPath);
|
|
9378
|
-
fsyncDirectory(
|
|
9543
|
+
fsyncDirectory(resolve5(lockPath, ".."));
|
|
9379
9544
|
} catch {}
|
|
9380
9545
|
}
|
|
9381
9546
|
function observeStaleWorkspaceLock(lockPath, workspaceRoot, processStartIdentityLookup = processStartIdentity) {
|
|
@@ -9446,7 +9611,7 @@ function tryTakeoverStaleWorkspaceLock(candidatePath, lockPath, workspaceRoot, c
|
|
|
9446
9611
|
const candidateInstalled = !current.isSymbolicLink() && current.dev === candidateIdentity.dev && current.ino === candidateIdentity.ino && currentFileHash(lockPath, workspaceRoot) === candidateHash;
|
|
9447
9612
|
const staleDisplaced = !displaced.isSymbolicLink() && displaced.dev === stale.identity.dev && displaced.ino === stale.identity.ino && currentFileHash(candidatePath, workspaceRoot) === stale.contentHash;
|
|
9448
9613
|
if (!candidateInstalled || !staleDisplaced) {
|
|
9449
|
-
if (candidateInstalled &&
|
|
9614
|
+
if (candidateInstalled && existsSync8(candidatePath)) {
|
|
9450
9615
|
atomicExchangePaths(candidatePath, lockPath);
|
|
9451
9616
|
exchanged = false;
|
|
9452
9617
|
return false;
|
|
@@ -9454,13 +9619,13 @@ function tryTakeoverStaleWorkspaceLock(candidatePath, lockPath, workspaceRoot, c
|
|
|
9454
9619
|
throw new ProjectContextError("PROJECT_CONTEXT_LOCK_LOST", "workspace lock changed during stale-lock takeover and could not be restored safely");
|
|
9455
9620
|
}
|
|
9456
9621
|
rmSync2(candidatePath);
|
|
9457
|
-
fsyncDirectory(
|
|
9622
|
+
fsyncDirectory(resolve5(lockPath, ".."));
|
|
9458
9623
|
exchanged = false;
|
|
9459
9624
|
return true;
|
|
9460
9625
|
} catch (error) {
|
|
9461
9626
|
if (exchanged) {
|
|
9462
9627
|
try {
|
|
9463
|
-
if (currentFileHash(lockPath, workspaceRoot) === candidateHash &&
|
|
9628
|
+
if (currentFileHash(lockPath, workspaceRoot) === candidateHash && existsSync8(candidatePath)) {
|
|
9464
9629
|
atomicExchangePaths(candidatePath, lockPath);
|
|
9465
9630
|
exchanged = false;
|
|
9466
9631
|
}
|
|
@@ -9473,7 +9638,7 @@ function tryTakeoverStaleWorkspaceLock(candidatePath, lockPath, workspaceRoot, c
|
|
|
9473
9638
|
}
|
|
9474
9639
|
}
|
|
9475
9640
|
function assertWorkspaceLockHeld(lockPath, lock, workspaceRoot) {
|
|
9476
|
-
if (!
|
|
9641
|
+
if (!existsSync8(lockPath)) {
|
|
9477
9642
|
throw new ProjectContextError("PROJECT_CONTEXT_LOCK_LOST", "workspace project-context lock changed during render");
|
|
9478
9643
|
}
|
|
9479
9644
|
const current = lstatSync(lockPath);
|
|
@@ -9533,8 +9698,8 @@ function releaseWorkspaceLock(lockPath, lock, workspaceRoot) {
|
|
|
9533
9698
|
}
|
|
9534
9699
|
return;
|
|
9535
9700
|
}
|
|
9536
|
-
const lockDirectory =
|
|
9537
|
-
const releasePath =
|
|
9701
|
+
const lockDirectory = resolve5(lockPath, "..");
|
|
9702
|
+
const releasePath = join8(lockDirectory, `.project-context-release-${randomUUID5()}.tmp`);
|
|
9538
9703
|
let releaseFd = null;
|
|
9539
9704
|
let releaseIdentity = null;
|
|
9540
9705
|
let releaseHash = null;
|
|
@@ -9563,7 +9728,7 @@ function releaseWorkspaceLock(lockPath, lock, workspaceRoot) {
|
|
|
9563
9728
|
const releaseInstalled = !installed.isSymbolicLink() && installed.dev === releaseIdentity.dev && installed.ino === releaseIdentity.ino && currentFileHash(lockPath, workspaceRoot) === releaseHash;
|
|
9564
9729
|
const ownedDisplaced = !displaced.isSymbolicLink() && displaced.dev === lock.identity.dev && displaced.ino === lock.identity.ino && currentFileHash(releasePath, workspaceRoot) === lock.contentHash;
|
|
9565
9730
|
if (!releaseInstalled || !ownedDisplaced) {
|
|
9566
|
-
if (releaseInstalled &&
|
|
9731
|
+
if (releaseInstalled && existsSync8(releasePath)) {
|
|
9567
9732
|
atomicExchangePaths(releasePath, lockPath);
|
|
9568
9733
|
exchanged = false;
|
|
9569
9734
|
}
|
|
@@ -9576,7 +9741,7 @@ function releaseWorkspaceLock(lockPath, lock, workspaceRoot) {
|
|
|
9576
9741
|
} catch {
|
|
9577
9742
|
if (exchanged) {
|
|
9578
9743
|
try {
|
|
9579
|
-
if (releaseHash && currentFileHash(lockPath, workspaceRoot) === releaseHash &&
|
|
9744
|
+
if (releaseHash && currentFileHash(lockPath, workspaceRoot) === releaseHash && existsSync8(releasePath)) {
|
|
9580
9745
|
atomicExchangePaths(releasePath, lockPath);
|
|
9581
9746
|
exchanged = false;
|
|
9582
9747
|
}
|
|
@@ -9588,7 +9753,7 @@ function releaseWorkspaceLock(lockPath, lock, workspaceRoot) {
|
|
|
9588
9753
|
closeSync(releaseFd);
|
|
9589
9754
|
} catch {}
|
|
9590
9755
|
}
|
|
9591
|
-
if (!exchanged &&
|
|
9756
|
+
if (!exchanged && existsSync8(releasePath)) {
|
|
9592
9757
|
try {
|
|
9593
9758
|
rmSync2(releasePath);
|
|
9594
9759
|
} catch {}
|
|
@@ -9614,15 +9779,15 @@ function ensureSafeDirectory(path, workspaceRoot, mode) {
|
|
|
9614
9779
|
const segments = rel.split(/[\\/]+/).filter(Boolean);
|
|
9615
9780
|
let current = workspaceRoot;
|
|
9616
9781
|
for (const segment of segments) {
|
|
9617
|
-
current =
|
|
9618
|
-
if (
|
|
9782
|
+
current = join8(current, segment);
|
|
9783
|
+
if (existsSync8(current)) {
|
|
9619
9784
|
if (lstatSync(current).isSymbolicLink())
|
|
9620
9785
|
throw new ProjectContextError("PROJECT_CONTEXT_SYMLINK_REJECTED", `managed path uses a symlink: ${current}`);
|
|
9621
9786
|
if (!statSync(current).isDirectory())
|
|
9622
9787
|
throw new ProjectContextError("PROJECT_CONTEXT_PATH_INVALID", `managed path is not a directory: ${current}`);
|
|
9623
9788
|
} else {
|
|
9624
9789
|
mkdirSync2(current, { mode });
|
|
9625
|
-
fsyncDirectory(
|
|
9790
|
+
fsyncDirectory(resolve5(current, ".."));
|
|
9626
9791
|
}
|
|
9627
9792
|
}
|
|
9628
9793
|
}
|
|
@@ -9678,11 +9843,11 @@ function scanGeneratedContent(content) {
|
|
|
9678
9843
|
function runtimePaths(workspaceRoot, runtime) {
|
|
9679
9844
|
const relativeTarget = runtime === "claude" ? "CLAUDE.md" : runtime === "codewith" ? ".codewith/CODEWITH.md" : "AGENTS.md";
|
|
9680
9845
|
return {
|
|
9681
|
-
target:
|
|
9682
|
-
fragment:
|
|
9683
|
-
manifest:
|
|
9684
|
-
cache:
|
|
9685
|
-
sessionManifest: runtime === "codewith" ?
|
|
9846
|
+
target: resolve5(workspaceRoot, ...relativeTarget.split("/")),
|
|
9847
|
+
fragment: resolve5(workspaceRoot, ...PROJECT_CONTEXT_FRAGMENT_PATH.split("/")),
|
|
9848
|
+
manifest: resolve5(workspaceRoot, ...PROJECT_CONTEXT_MANIFEST_PATH.split("/")),
|
|
9849
|
+
cache: resolve5(workspaceRoot, ...PROJECT_CONTEXT_CACHE_PATH.split("/")),
|
|
9850
|
+
sessionManifest: runtime === "codewith" ? resolve5(workspaceRoot, ".codewith", ".hasna", "session-render-manifest.json") : resolve5(workspaceRoot, ".hasna", "session-render-manifest.json")
|
|
9686
9851
|
};
|
|
9687
9852
|
}
|
|
9688
9853
|
function projectContextSessionGuardPaths(paths, runtime) {
|
|
@@ -9692,7 +9857,7 @@ function projectContextSessionGuardPaths(paths, runtime) {
|
|
|
9692
9857
|
paths.fragment,
|
|
9693
9858
|
paths.target,
|
|
9694
9859
|
paths.sessionManifest,
|
|
9695
|
-
...runtime === "codewith" ? [
|
|
9860
|
+
...runtime === "codewith" ? [resolve5(paths.target, "..", "CODEWITH.override.md")] : []
|
|
9696
9861
|
];
|
|
9697
9862
|
}
|
|
9698
9863
|
function sessionTargetRelativePath(runtime) {
|
|
@@ -9712,27 +9877,27 @@ function projectContextRuntimeForSessionTool(tool) {
|
|
|
9712
9877
|
return null;
|
|
9713
9878
|
}
|
|
9714
9879
|
function projectContextWorkspaceForSession(input, runtime) {
|
|
9715
|
-
const targetHome =
|
|
9880
|
+
const targetHome = resolve5(input.target_home);
|
|
9716
9881
|
if (runtime === "codewith") {
|
|
9717
|
-
const workspaceRoot = basename(targetHome) === ".codewith" ?
|
|
9882
|
+
const workspaceRoot = basename(targetHome) === ".codewith" ? dirname2(targetHome) : null;
|
|
9718
9883
|
if (!workspaceRoot)
|
|
9719
9884
|
return null;
|
|
9720
|
-
if (input.project_root &&
|
|
9885
|
+
if (input.project_root && resolve5(input.project_root) !== workspaceRoot) {
|
|
9721
9886
|
throw new ProjectContextError("PROJECT_CONTEXT_PATH_INVALID", "Codewith project_root must be the parent workspace of target_home");
|
|
9722
9887
|
}
|
|
9723
|
-
if (!
|
|
9888
|
+
if (!existsSync8(workspaceRoot) || !lstatSync(workspaceRoot).isDirectory())
|
|
9724
9889
|
return null;
|
|
9725
9890
|
return assertSafeWorkspaceRoot(workspaceRoot);
|
|
9726
9891
|
}
|
|
9727
|
-
if (!
|
|
9892
|
+
if (!existsSync8(targetHome) || !lstatSync(targetHome).isDirectory())
|
|
9728
9893
|
return null;
|
|
9729
9894
|
return assertSafeWorkspaceRoot(targetHome);
|
|
9730
9895
|
}
|
|
9731
9896
|
function assertCodewithTargetIsConsumed(workspaceRoot, runtime) {
|
|
9732
9897
|
if (runtime !== "codewith")
|
|
9733
9898
|
return;
|
|
9734
|
-
const override =
|
|
9735
|
-
if (!
|
|
9899
|
+
const override = resolve5(workspaceRoot, ".codewith", "CODEWITH.override.md");
|
|
9900
|
+
if (!existsSync8(override))
|
|
9736
9901
|
return;
|
|
9737
9902
|
assertNoSymlinkSegments(workspaceRoot, override);
|
|
9738
9903
|
if (!lstatSync(override).isFile())
|
|
@@ -9742,10 +9907,10 @@ function assertCodewithTargetIsConsumed(workspaceRoot, runtime) {
|
|
|
9742
9907
|
function assertSafeWorkspaceRoot(path) {
|
|
9743
9908
|
if (!isAbsolute(path))
|
|
9744
9909
|
throw new ProjectContextError("PROJECT_CONTEXT_PATH_INVALID", "workspace root must be absolute");
|
|
9745
|
-
const normalized =
|
|
9910
|
+
const normalized = resolve5(path);
|
|
9746
9911
|
if (normalized === parse(normalized).root)
|
|
9747
9912
|
throw new ProjectContextError("PROJECT_CONTEXT_PATH_INVALID", "workspace root cannot be the filesystem root");
|
|
9748
|
-
if (!
|
|
9913
|
+
if (!existsSync8(normalized) || !lstatSync(normalized).isDirectory())
|
|
9749
9914
|
throw new ProjectContextError("PROJECT_CONTEXT_PATH_INVALID", "workspace root must be an existing directory");
|
|
9750
9915
|
assertNoSymlinkAncestors(normalized);
|
|
9751
9916
|
if (lstatSync(normalized).isSymbolicLink())
|
|
@@ -9759,18 +9924,18 @@ function assertNoSymlinkSegments(root, target) {
|
|
|
9759
9924
|
}
|
|
9760
9925
|
let current = root;
|
|
9761
9926
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
9762
|
-
current =
|
|
9763
|
-
if (
|
|
9927
|
+
current = join8(current, segment);
|
|
9928
|
+
if (existsSync8(current) && lstatSync(current).isSymbolicLink()) {
|
|
9764
9929
|
throw new ProjectContextError("PROJECT_CONTEXT_SYMLINK_REJECTED", `managed path uses a symlink: ${current}`);
|
|
9765
9930
|
}
|
|
9766
9931
|
}
|
|
9767
9932
|
}
|
|
9768
9933
|
function assertNoSymlinkAncestors(path) {
|
|
9769
|
-
const normalized =
|
|
9934
|
+
const normalized = resolve5(path);
|
|
9770
9935
|
let current = parse(normalized).root;
|
|
9771
9936
|
for (const segment of relative(current, normalized).split(/[\\/]+/).filter(Boolean)) {
|
|
9772
|
-
current =
|
|
9773
|
-
if (!
|
|
9937
|
+
current = join8(current, segment);
|
|
9938
|
+
if (!existsSync8(current))
|
|
9774
9939
|
return;
|
|
9775
9940
|
if (lstatSync(current).isSymbolicLink())
|
|
9776
9941
|
throw new ProjectContextError("PROJECT_CONTEXT_SYMLINK_REJECTED", `workspace ancestor is a symlink: ${current}`);
|
|
@@ -9786,7 +9951,7 @@ function readUtf8RegularFile(path, workspaceRoot, maxBytes = FOREIGN_INPUT_MAX_B
|
|
|
9786
9951
|
return readFileSync(path, "utf8");
|
|
9787
9952
|
}
|
|
9788
9953
|
function currentFileHash(path, workspaceRoot) {
|
|
9789
|
-
if (!
|
|
9954
|
+
if (!existsSync8(path))
|
|
9790
9955
|
return null;
|
|
9791
9956
|
const relativePath = relativePosix(workspaceRoot, path);
|
|
9792
9957
|
return sha2562(readUtf8RegularFile(path, workspaceRoot, managedObservationMaxBytes(relativePath)));
|
|
@@ -9805,10 +9970,10 @@ function fragmentMatchesBundle(path, bundle, workspaceRoot) {
|
|
|
9805
9970
|
}
|
|
9806
9971
|
function durableSourcePath(path, workspaceRoot) {
|
|
9807
9972
|
if (!path || path.startsWith("/dev/fd/"))
|
|
9808
|
-
return
|
|
9809
|
-
const normalized = isAbsolute(path) ?
|
|
9973
|
+
return resolve5(workspaceRoot, ...PROJECT_CONTEXT_CACHE_PATH.split("/"));
|
|
9974
|
+
const normalized = isAbsolute(path) ? resolve5(path) : resolve5(workspaceRoot, path);
|
|
9810
9975
|
if (normalized.startsWith("/dev/fd/"))
|
|
9811
|
-
return
|
|
9976
|
+
return resolve5(workspaceRoot, ...PROJECT_CONTEXT_CACHE_PATH.split("/"));
|
|
9812
9977
|
return normalized;
|
|
9813
9978
|
}
|
|
9814
9979
|
function compareRevisions(incoming, previous) {
|
|
@@ -9975,11 +10140,12 @@ function sha2562(content) {
|
|
|
9975
10140
|
function isRecord(value) {
|
|
9976
10141
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
9977
10142
|
}
|
|
9978
|
-
var PROJECT_CONTEXT_SCHEMA = "hasna.projects.project_context_bundle.v1", PROJECT_CONTEXT_SCHEMA_V2 = "hasna.projects.project_context_bundle.v2", PROJECT_CONTEXT_SUPPORTED_SCHEMAS, projectContextSchema, PROJECT_CONTEXT_MAX_INPUT_BYTES, PROJECT_CONTEXT_MAX_RENDERED_BYTES, PROJECT_CONTEXT_MAX_APPROX_TOKENS = 1000, PROJECT_CONTEXT_MAX_COMMANDS = 6, PROJECT_CONTEXT_MAX_WARNINGS = 3, PROJECT_CONTEXT_FRAGMENT_PATH = ".hasna/instructions/project-context.md", PROJECT_CONTEXT_MANIFEST_PATH = ".hasna/project-context-manifest.json", PROJECT_CONTEXT_CACHE_PATH = ".hasna/project-context-cache.json", PROJECT_CONTEXT_LOCK_PATH = ".hasna/project-context.lock", PROJECT_CONTEXT_SNAPSHOT_DIR = ".hasna/project-context-snapshots", PROJECT_CONTEXT_CACHE_SCHEMA = "hasna.instructions.project-context-cache/v1", PROJECT_CONTEXT_MANAGED_COMMENT = "Managed by @hasna/configs project context", SESSION_COMPATIBILITY_MANIFEST_MAX_BYTES, FOREIGN_INPUT_MAX_BYTES, SESSION_MANAGED_OUTPUT_MAX_BYTES, SESSION_MANAGED_INPUT_MAX_BYTES, SESSION_MANAGED_OUTPUT_PATHS, SESSION_MANAGED_OUTPUT_WARN_BYTES, PROJECT_CONTEXT_LOCK_STALE_MS, LEGACY_CONFIGS_PACKAGE = "@hasna/configs", LEGACY_CONFIGS_COMPAT_VERSION = "0.2.45", LEGACY_CONFIGS_EXECUTABLE = "configs", PROJECT_KINDS, PROJECT_STATUSES, LINK_STATES, RESOLUTION_SOURCES, safeId, nullableId, producerSlug, producerName, safeOptionalDisplay, isoTimestamp, revisionSchema, hashSchema, absolutePath, commandArg, financeText, financeLegalEntity, financeProjectMetadataSchema, commandSchema, projectContextBundleSchema, storedManifestProjectContextSchema, storedManifestFileSchema, storedManifestObservationSchema, projectContextMetadataSnapshotSchema, projectContextCacheSchema, ProjectContextError, ProjectContextHashRace, anchoredFsOps, atomicExchange, atomicExchangeLibraries;
|
|
10143
|
+
var PROJECT_CONTEXT_SCHEMA = "hasna.projects.project_context_bundle.v1", PROJECT_CONTEXT_SCHEMA_V2 = "hasna.projects.project_context_bundle.v2", PROJECT_CONTEXT_SUPPORTED_SCHEMAS, projectContextSchema, PROJECT_CONTEXT_MAX_INPUT_BYTES, PROJECT_CONTEXT_MAX_RENDERED_BYTES, PROJECT_CONTEXT_MAX_APPROX_TOKENS = 1000, PROJECT_CONTEXT_MAX_COMMANDS = 6, PROJECT_CONTEXT_MAX_WARNINGS = 3, PROJECT_CONTEXT_FRAGMENT_PATH = ".hasna/instructions/project-context.md", PROJECT_CONTEXT_MANIFEST_PATH = ".hasna/projects/project-context-manifest.json", PROJECT_CONTEXT_CACHE_PATH = ".hasna/projects/project-context-cache.json", PROJECT_CONTEXT_LOCK_PATH = ".hasna/projects/project-context.lock", PROJECT_CONTEXT_SNAPSHOT_DIR = ".hasna/projects/project-context-snapshots", PROJECT_CONTEXT_CACHE_SCHEMA = "hasna.instructions.project-context-cache/v1", PROJECT_CONTEXT_MANAGED_COMMENT = "Managed by @hasna/configs project context", SESSION_COMPATIBILITY_MANIFEST_MAX_BYTES, FOREIGN_INPUT_MAX_BYTES, SESSION_MANAGED_OUTPUT_MAX_BYTES, SESSION_MANAGED_INPUT_MAX_BYTES, SESSION_MANAGED_OUTPUT_PATHS, SESSION_MANAGED_OUTPUT_WARN_BYTES, PROJECT_CONTEXT_LOCK_STALE_MS, LEGACY_CONFIGS_PACKAGE = "@hasna/configs", LEGACY_CONFIGS_COMPAT_VERSION = "0.2.45", LEGACY_CONFIGS_EXECUTABLE = "configs", PROJECT_KINDS, PROJECT_STATUSES, LINK_STATES, RESOLUTION_SOURCES, safeId, nullableId, producerSlug, producerName, safeOptionalDisplay, isoTimestamp, revisionSchema, hashSchema, absolutePath, commandArg, financeText, financeLegalEntity, financeProjectMetadataSchema, commandSchema, projectContextBundleSchema, storedManifestProjectContextSchema, storedManifestFileSchema, storedManifestObservationSchema, projectContextMetadataSnapshotSchema, projectContextCacheSchema, ProjectContextError, ProjectContextHashRace, anchoredFsOps, atomicExchange, atomicExchangeLibraries;
|
|
9979
10144
|
var init_project_context = __esm(() => {
|
|
9980
10145
|
init_zod();
|
|
9981
10146
|
init_redact();
|
|
9982
10147
|
init_session_render_contract();
|
|
10148
|
+
init_session_render_state();
|
|
9983
10149
|
PROJECT_CONTEXT_SUPPORTED_SCHEMAS = [PROJECT_CONTEXT_SCHEMA, PROJECT_CONTEXT_SCHEMA_V2];
|
|
9984
10150
|
projectContextSchema = exports_external.enum(PROJECT_CONTEXT_SUPPORTED_SCHEMAS);
|
|
9985
10151
|
PROJECT_CONTEXT_MAX_INPUT_BYTES = 8 * 1024;
|
|
@@ -10368,7 +10534,7 @@ function compareProviderVersions(left, right) {
|
|
|
10368
10534
|
|
|
10369
10535
|
// src/lib/asset-plan.ts
|
|
10370
10536
|
import { createHash as createHash3 } from "crypto";
|
|
10371
|
-
import { isAbsolute as isAbsolute2, posix, resolve as
|
|
10537
|
+
import { isAbsolute as isAbsolute2, posix, resolve as resolve6 } from "path";
|
|
10372
10538
|
function assetCapability(provider, surface, kind, support, strategies, note, providerVersionRange = "*") {
|
|
10373
10539
|
return Object.freeze({
|
|
10374
10540
|
schema: ASSET_CAPABILITY_SCHEMA,
|
|
@@ -10575,8 +10741,8 @@ function resolveAssetDestination(item, roots) {
|
|
|
10575
10741
|
if (!isAbsolute2(root))
|
|
10576
10742
|
throw new Error(`Asset ${item.assetKey} destination root must be absolute.`);
|
|
10577
10743
|
const relativePath = safeRelativePath(item.destination.relativePath);
|
|
10578
|
-
const target =
|
|
10579
|
-
const normalizedRoot =
|
|
10744
|
+
const target = resolve6(root, ...relativePath.split("/"));
|
|
10745
|
+
const normalizedRoot = resolve6(root);
|
|
10580
10746
|
if (target === normalizedRoot)
|
|
10581
10747
|
throw new Error(`Asset ${item.assetKey} destination cannot replace its root.`);
|
|
10582
10748
|
if (!target.startsWith(`${normalizedRoot}/`))
|
|
@@ -10756,13 +10922,13 @@ var init_asset_plan = __esm(() => {
|
|
|
10756
10922
|
// src/lib/cursor-authority.ts
|
|
10757
10923
|
import { createHash as createHash4 } from "crypto";
|
|
10758
10924
|
import { lstatSync as lstatSync2, readFileSync as readFileSync2 } from "fs";
|
|
10759
|
-
import { homedir as
|
|
10760
|
-
import { join as
|
|
10925
|
+
import { homedir as homedir5 } from "os";
|
|
10926
|
+
import { join as join9, resolve as resolve7 } from "path";
|
|
10761
10927
|
function sha2564(content) {
|
|
10762
10928
|
return createHash4("sha256").update(content).digest("hex");
|
|
10763
10929
|
}
|
|
10764
|
-
function
|
|
10765
|
-
return process.env["HOME"] ||
|
|
10930
|
+
function homeDir3() {
|
|
10931
|
+
return process.env["HOME"] || homedir5();
|
|
10766
10932
|
}
|
|
10767
10933
|
function markerPayload(content, markerLine, markerIndex) {
|
|
10768
10934
|
const index = markerIndex ?? content.indexOf(markerLine);
|
|
@@ -10778,12 +10944,12 @@ function baseObservation(path) {
|
|
|
10778
10944
|
};
|
|
10779
10945
|
}
|
|
10780
10946
|
function observeCursorGlobalAuthority(options = {}) {
|
|
10781
|
-
const authorityPath =
|
|
10947
|
+
const authorityPath = resolve7(join9(options.home ?? homeDir3(), CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH));
|
|
10782
10948
|
const readFile2 = options.readFile ?? ((path) => readFileSync2(path, "utf8"));
|
|
10783
10949
|
return observeCursorGlobalAuthorityPath(authorityPath, readFile2);
|
|
10784
10950
|
}
|
|
10785
10951
|
function observeCursorGlobalAuthorityAtPath(authorityPath) {
|
|
10786
|
-
return observeCursorGlobalAuthorityPath(
|
|
10952
|
+
return observeCursorGlobalAuthorityPath(resolve7(authorityPath), (path) => readFileSync2(path, "utf8"));
|
|
10787
10953
|
}
|
|
10788
10954
|
function observeCursorGlobalAuthorityPath(authorityPath, readFile2) {
|
|
10789
10955
|
const base = baseObservation(authorityPath);
|
|
@@ -10928,7 +11094,7 @@ function observeCursorGlobalAuthorityPath(authorityPath, readFile2) {
|
|
|
10928
11094
|
};
|
|
10929
11095
|
}
|
|
10930
11096
|
function isCursorGlobalAuthorityPath(path) {
|
|
10931
|
-
return
|
|
11097
|
+
return resolve7(path) === resolve7(join9(homeDir3(), CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH));
|
|
10932
11098
|
}
|
|
10933
11099
|
function stampCursorGlobalAuthorityMarker(content) {
|
|
10934
11100
|
const existing = content.match(CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN);
|
|
@@ -10984,16 +11150,16 @@ var init_cursor_authority = __esm(() => {
|
|
|
10984
11150
|
// src/lib/session-authority.ts
|
|
10985
11151
|
import { createHash as createHash5 } from "crypto";
|
|
10986
11152
|
import { lstatSync as lstatSync3, readFileSync as readFileSync3, realpathSync, statSync as statSync2 } from "fs";
|
|
10987
|
-
import { homedir as
|
|
10988
|
-
import { join as
|
|
11153
|
+
import { homedir as homedir6 } from "os";
|
|
11154
|
+
import { join as join10, resolve as resolve8 } from "path";
|
|
10989
11155
|
function sha2565(content) {
|
|
10990
11156
|
return createHash5("sha256").update(content).digest("hex");
|
|
10991
11157
|
}
|
|
10992
11158
|
function configHomeDir() {
|
|
10993
|
-
return process.env["CONFIGS_HOME"] || process.env["HOME"] ||
|
|
11159
|
+
return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir6();
|
|
10994
11160
|
}
|
|
10995
11161
|
function normalizeOwnedTargetPath(p) {
|
|
10996
|
-
const expanded = p.startsWith("~/") ?
|
|
11162
|
+
const expanded = p.startsWith("~/") ? resolve8(configHomeDir(), p.slice(2)) : resolve8(p);
|
|
10997
11163
|
try {
|
|
10998
11164
|
return realpathSync(expanded);
|
|
10999
11165
|
} catch {
|
|
@@ -11001,7 +11167,7 @@ function normalizeOwnedTargetPath(p) {
|
|
|
11001
11167
|
}
|
|
11002
11168
|
}
|
|
11003
11169
|
function detectClaudeAuthorityConflicts(targetHome, ownedAuthorities = []) {
|
|
11004
|
-
const authorityPath =
|
|
11170
|
+
const authorityPath = resolve8(join10(targetHome, CLAUDE_LEGACY_AUTHORITY_RELATIVE_PATH));
|
|
11005
11171
|
let stat;
|
|
11006
11172
|
try {
|
|
11007
11173
|
stat = lstatSync3(authorityPath);
|
|
@@ -11090,9 +11256,9 @@ var init_session_authority = __esm(() => {
|
|
|
11090
11256
|
|
|
11091
11257
|
// src/lib/session-render.ts
|
|
11092
11258
|
import { createHash as createHash6 } from "crypto";
|
|
11093
|
-
import { existsSync as
|
|
11094
|
-
import { homedir as
|
|
11095
|
-
import { basename as basename4, dirname as
|
|
11259
|
+
import { existsSync as existsSync10, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync3 } from "fs";
|
|
11260
|
+
import { homedir as homedir7 } from "os";
|
|
11261
|
+
import { basename as basename4, dirname as dirname4, extname as extname2, isAbsolute as isAbsolute3, join as join11, parse as parse2, posix as posix2, relative as relative2, resolve as resolve9 } from "path";
|
|
11096
11262
|
function normalizeSessionInstructionLayer(value) {
|
|
11097
11263
|
if (value === "provider")
|
|
11098
11264
|
return "tool";
|
|
@@ -11178,13 +11344,13 @@ function yamlQuote2(value) {
|
|
|
11178
11344
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
11179
11345
|
}
|
|
11180
11346
|
function defaultTargetHome(tool, profile, sessionId) {
|
|
11181
|
-
const home = process.env["HOME"] ||
|
|
11182
|
-
return
|
|
11347
|
+
const home = process.env["HOME"] || homedir7();
|
|
11348
|
+
return join11(home, ".hasna", "accounts", "profiles", tool, slug(profile));
|
|
11183
11349
|
}
|
|
11184
11350
|
function joinTarget(targetHome, relativePath) {
|
|
11185
11351
|
const safeTargetHome = assertSafeTargetRoot(targetHome);
|
|
11186
11352
|
const safeRelativePath2 = assertSafeRelativePath(relativePath);
|
|
11187
|
-
return
|
|
11353
|
+
return join11(safeTargetHome, ...safeRelativePath2.split("/"));
|
|
11188
11354
|
}
|
|
11189
11355
|
function makeFile(targetHome, relativePath, role, content, sourceIds) {
|
|
11190
11356
|
const safeTargetHome = assertSafeTargetRoot(targetHome);
|
|
@@ -11753,7 +11919,7 @@ function buildOpenCodeFiles(targetHome, adapter, profile, sources, providerConfi
|
|
|
11753
11919
|
...sources.flatMap((source) => source.resolvedRules.map((rule) => rule.id))
|
|
11754
11920
|
]);
|
|
11755
11921
|
const existingConfigPath = joinTarget(targetHome, adapter.configFile);
|
|
11756
|
-
const selectedConfig =
|
|
11922
|
+
const selectedConfig = existsSync10(existingConfigPath) ? readOpenCodeConfig(readFileSync4(existingConfigPath, "utf8"), existingConfigPath) : providerConfig ? readOpenCodeConfig(providerConfig.content, providerConfig.sourceId) : {};
|
|
11757
11923
|
const preservedInstructions = normalizeOpenCodeInstructions(selectedConfig["instructions"]).filter((path) => !pathIsManagedOpenCodeInstruction(path, adapter.managedDir));
|
|
11758
11924
|
const config = {
|
|
11759
11925
|
...selectedConfig,
|
|
@@ -11942,7 +12108,7 @@ function adapterFor(input) {
|
|
|
11942
12108
|
return gatedNativeImports ? CODEWITH_NATIVE_ADAPTER : CODEWITH_FLATTENED_ADAPTER;
|
|
11943
12109
|
}
|
|
11944
12110
|
function getHomeDir() {
|
|
11945
|
-
return process.env["CONFIGS_HOME"] || process.env["HOME"] ||
|
|
12111
|
+
return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir7();
|
|
11946
12112
|
}
|
|
11947
12113
|
function cleanSessionPathInput(path) {
|
|
11948
12114
|
const trimmed = path.trim();
|
|
@@ -11957,16 +12123,16 @@ function resolveSessionPath(path) {
|
|
|
11957
12123
|
throw new Error("Session render path cannot be empty.");
|
|
11958
12124
|
const home = getHomeDir();
|
|
11959
12125
|
if (cleaned === "~")
|
|
11960
|
-
return
|
|
12126
|
+
return resolve9(home);
|
|
11961
12127
|
if (cleaned.startsWith("~/"))
|
|
11962
|
-
return
|
|
12128
|
+
return resolve9(home, cleaned.slice(2));
|
|
11963
12129
|
if (cleaned === "{{HOME}}" || cleaned === "${HOME}")
|
|
11964
|
-
return
|
|
12130
|
+
return resolve9(home);
|
|
11965
12131
|
if (cleaned.startsWith("{{HOME}}/"))
|
|
11966
|
-
return
|
|
12132
|
+
return resolve9(home, cleaned.slice("{{HOME}}/".length));
|
|
11967
12133
|
if (cleaned.startsWith("${HOME}/"))
|
|
11968
|
-
return
|
|
11969
|
-
return
|
|
12134
|
+
return resolve9(home, cleaned.slice("${HOME}/".length));
|
|
12135
|
+
return resolve9(cleaned);
|
|
11970
12136
|
}
|
|
11971
12137
|
function assertSafeRelativePath(relativePath) {
|
|
11972
12138
|
if (!relativePath.trim())
|
|
@@ -11982,7 +12148,7 @@ function assertSafeRelativePath(relativePath) {
|
|
|
11982
12148
|
function assertSafeTargetRoot(targetHome) {
|
|
11983
12149
|
if (!isAbsolute3(targetHome))
|
|
11984
12150
|
throw new Error(`Session render target must be an absolute path: ${targetHome}`);
|
|
11985
|
-
const normalized =
|
|
12151
|
+
const normalized = resolve9(targetHome);
|
|
11986
12152
|
if (normalized === parse2(normalized).root) {
|
|
11987
12153
|
throw new Error(`Session render target cannot be the filesystem root: ${targetHome}`);
|
|
11988
12154
|
}
|
|
@@ -12218,7 +12384,7 @@ function planSessionRender(input) {
|
|
|
12218
12384
|
sourceId: input.providerConfig.sourceId,
|
|
12219
12385
|
selectedPayloadSha256: sha2566(input.providerConfig.content),
|
|
12220
12386
|
renderedPayloadSha256: files.find((file) => file.relativePath === adapter.configFile)?.sha256 ?? sha2566(input.providerConfig.content),
|
|
12221
|
-
selected: !
|
|
12387
|
+
selected: !existsSync10(joinTarget(targetHome, adapter.configFile))
|
|
12222
12388
|
}
|
|
12223
12389
|
} : {},
|
|
12224
12390
|
...projectContext ? {
|
|
@@ -12479,10 +12645,10 @@ function layerFromIdentityKind(kind, exportShape) {
|
|
|
12479
12645
|
function contentFromIdentitySourcePaths(sourcePaths, exportPath, sourceId) {
|
|
12480
12646
|
if (sourcePaths.length === 0 || !exportPath)
|
|
12481
12647
|
return;
|
|
12482
|
-
const
|
|
12648
|
+
const baseDir = dirname4(resolveSessionPath(exportPath));
|
|
12483
12649
|
const contents = [];
|
|
12484
12650
|
for (const sourcePath of sourcePaths) {
|
|
12485
|
-
const content = readIdentitySourcePath(sourcePath,
|
|
12651
|
+
const content = readIdentitySourcePath(sourcePath, baseDir, sourceId);
|
|
12486
12652
|
if (content !== undefined)
|
|
12487
12653
|
contents.push({ path: sourcePath.path, content });
|
|
12488
12654
|
}
|
|
@@ -12495,9 +12661,9 @@ ${item.content.trimEnd()}`).join(`
|
|
|
12495
12661
|
|
|
12496
12662
|
`));
|
|
12497
12663
|
}
|
|
12498
|
-
function readIdentitySourcePath(sourcePath,
|
|
12499
|
-
const resolvedPath = resolveIdentitySourcePath(sourcePath.path,
|
|
12500
|
-
if (!
|
|
12664
|
+
function readIdentitySourcePath(sourcePath, baseDir, sourceId) {
|
|
12665
|
+
const resolvedPath = resolveIdentitySourcePath(sourcePath.path, baseDir, sourceId);
|
|
12666
|
+
if (!existsSync10(resolvedPath)) {
|
|
12501
12667
|
if (sourcePath.required) {
|
|
12502
12668
|
throw new Error(`Required identity instruction source path not found for ${sourceId}: ${sourcePath.path}`);
|
|
12503
12669
|
}
|
|
@@ -12507,27 +12673,27 @@ function readIdentitySourcePath(sourcePath, baseDir3, sourceId) {
|
|
|
12507
12673
|
if (!stat.isFile()) {
|
|
12508
12674
|
throw new Error(`Identity instruction source path is not a file for ${sourceId}: ${sourcePath.path}`);
|
|
12509
12675
|
}
|
|
12510
|
-
const realBase = realpathSync2(
|
|
12676
|
+
const realBase = realpathSync2(baseDir);
|
|
12511
12677
|
const realPath = realpathSync2(resolvedPath);
|
|
12512
12678
|
if (!pathIsInside(realPath, realBase)) {
|
|
12513
12679
|
throw new Error(`Identity instruction source path escapes export directory for ${sourceId}: ${sourcePath.path}`);
|
|
12514
12680
|
}
|
|
12515
12681
|
return readFileSync4(realPath, "utf-8");
|
|
12516
12682
|
}
|
|
12517
|
-
function resolveIdentitySourcePath(path,
|
|
12683
|
+
function resolveIdentitySourcePath(path, baseDir, sourceId) {
|
|
12518
12684
|
const cleaned = cleanSessionPathInput(path);
|
|
12519
12685
|
if (!cleaned)
|
|
12520
12686
|
throw new Error(`Identity instruction source path cannot be empty for ${sourceId}.`);
|
|
12521
12687
|
if (cleaned.includes("\\"))
|
|
12522
12688
|
throw new Error(`Identity instruction source path must use POSIX separators for ${sourceId}: ${path}`);
|
|
12523
|
-
const resolvedPath = isAbsolute3(cleaned) ?
|
|
12524
|
-
if (!pathIsInside(resolvedPath,
|
|
12689
|
+
const resolvedPath = isAbsolute3(cleaned) ? resolve9(cleaned) : resolve9(baseDir, cleaned);
|
|
12690
|
+
if (!pathIsInside(resolvedPath, resolve9(baseDir))) {
|
|
12525
12691
|
throw new Error(`Identity instruction source path escapes export directory for ${sourceId}: ${path}`);
|
|
12526
12692
|
}
|
|
12527
12693
|
return resolvedPath;
|
|
12528
12694
|
}
|
|
12529
|
-
function pathIsInside(path,
|
|
12530
|
-
const rel = relative2(
|
|
12695
|
+
function pathIsInside(path, baseDir) {
|
|
12696
|
+
const rel = relative2(baseDir, path);
|
|
12531
12697
|
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
12532
12698
|
}
|
|
12533
12699
|
function providerTargetsTool(targets, tool) {
|
|
@@ -13628,6 +13794,1222 @@ var init_machines = __esm(() => {
|
|
|
13628
13794
|
init_database();
|
|
13629
13795
|
});
|
|
13630
13796
|
|
|
13797
|
+
// ../contracts/dist/client/storage.js
|
|
13798
|
+
import { isIP as isIP2 } from "net";
|
|
13799
|
+
import { spawnSync } from "child_process";
|
|
13800
|
+
import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync5 } from "fs";
|
|
13801
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
13802
|
+
import { createRequire as createRequire2 } from "module";
|
|
13803
|
+
import { hostname as osHostname } from "os";
|
|
13804
|
+
import { isAbsolute as isAbsolute4, join as join12 } from "path";
|
|
13805
|
+
function envToken2(name) {
|
|
13806
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
13807
|
+
}
|
|
13808
|
+
function clientTransportEnvKeys2(name) {
|
|
13809
|
+
const envSegment = envToken2(name);
|
|
13810
|
+
return {
|
|
13811
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
13812
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
13813
|
+
};
|
|
13814
|
+
}
|
|
13815
|
+
function credentialOverrideEnvKey2(name) {
|
|
13816
|
+
return `HASNA_${envToken2(name)}_API_KEY_OVERRIDE`;
|
|
13817
|
+
}
|
|
13818
|
+
function credentialPointerEnvKey2(name) {
|
|
13819
|
+
return `HASNA_${envToken2(name)}_API_KEY_REF`;
|
|
13820
|
+
}
|
|
13821
|
+
function homeDir4(env) {
|
|
13822
|
+
const home = env.HOME?.trim();
|
|
13823
|
+
return home ? home : null;
|
|
13824
|
+
}
|
|
13825
|
+
function absoluteOverride(env, key) {
|
|
13826
|
+
const value = env[key]?.trim();
|
|
13827
|
+
return value && isAbsolute4(value) ? value : null;
|
|
13828
|
+
}
|
|
13829
|
+
function hasnaHomeDir(env) {
|
|
13830
|
+
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
13831
|
+
if (override)
|
|
13832
|
+
return override;
|
|
13833
|
+
const home = homeDir4(env);
|
|
13834
|
+
return home ? join12(home, HASNA_HOME_DIR) : null;
|
|
13835
|
+
}
|
|
13836
|
+
function appConfigDir(name, env) {
|
|
13837
|
+
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
13838
|
+
if (configRoot)
|
|
13839
|
+
return join12(configRoot, name);
|
|
13840
|
+
const root = hasnaHomeDir(env);
|
|
13841
|
+
return root ? join12(root, name, CONFIG_SUBDIR) : null;
|
|
13842
|
+
}
|
|
13843
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
13844
|
+
if (!SAFE_APP_SLUG.test(name))
|
|
13845
|
+
return [];
|
|
13846
|
+
const directory = appConfigDir(name, env);
|
|
13847
|
+
if (!directory)
|
|
13848
|
+
return [];
|
|
13849
|
+
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
13850
|
+
return [{ path: join12(directory, file), tier: "disk" }];
|
|
13851
|
+
}
|
|
13852
|
+
function credentialDiskSources(name, env) {
|
|
13853
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
13854
|
+
}
|
|
13855
|
+
function profileDiskSources(name, env, profile) {
|
|
13856
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
13857
|
+
}
|
|
13858
|
+
function parseEnvFile(text) {
|
|
13859
|
+
const values = new Map;
|
|
13860
|
+
const unusable = new Set;
|
|
13861
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
13862
|
+
const line = rawLine.trim();
|
|
13863
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
13864
|
+
continue;
|
|
13865
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
13866
|
+
const equals = withoutExport.indexOf("=");
|
|
13867
|
+
if (equals <= 0)
|
|
13868
|
+
continue;
|
|
13869
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
13870
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
13871
|
+
continue;
|
|
13872
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
13873
|
+
const quote = value[0];
|
|
13874
|
+
if (quote === '"' || quote === "'") {
|
|
13875
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
13876
|
+
unusable.add(key);
|
|
13877
|
+
continue;
|
|
13878
|
+
}
|
|
13879
|
+
value = value.slice(1, -1);
|
|
13880
|
+
}
|
|
13881
|
+
if (value.trim().length === 0) {
|
|
13882
|
+
unusable.add(key);
|
|
13883
|
+
continue;
|
|
13884
|
+
}
|
|
13885
|
+
if (values.has(key) && values.get(key) !== value)
|
|
13886
|
+
unusable.add(key);
|
|
13887
|
+
values.set(key, value);
|
|
13888
|
+
}
|
|
13889
|
+
return { values, unusable };
|
|
13890
|
+
}
|
|
13891
|
+
function configFileModeAllowed(mode) {
|
|
13892
|
+
const permissions = mode & 4095;
|
|
13893
|
+
return permissions === 256 || permissions === 384;
|
|
13894
|
+
}
|
|
13895
|
+
function configFileReadsCoherent(before, after) {
|
|
13896
|
+
return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
|
|
13897
|
+
}
|
|
13898
|
+
function readAppConfigFile(path) {
|
|
13899
|
+
const unsafe = (reason) => {
|
|
13900
|
+
throw new CredentialFileUnsafeError(path, reason);
|
|
13901
|
+
};
|
|
13902
|
+
let fd = -1;
|
|
13903
|
+
try {
|
|
13904
|
+
fd = openSync2(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
13905
|
+
} catch (error) {
|
|
13906
|
+
const code = error.code;
|
|
13907
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
13908
|
+
return null;
|
|
13909
|
+
if (code === "ELOOP")
|
|
13910
|
+
unsafe("the path is a symlink");
|
|
13911
|
+
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
13912
|
+
}
|
|
13913
|
+
try {
|
|
13914
|
+
const before = fstatSync2(fd);
|
|
13915
|
+
if (!before.isFile())
|
|
13916
|
+
unsafe("the path is not a regular file");
|
|
13917
|
+
if (!configFileModeAllowed(before.mode)) {
|
|
13918
|
+
unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
|
|
13919
|
+
}
|
|
13920
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
13921
|
+
if (uid !== undefined && before.uid !== uid)
|
|
13922
|
+
unsafe("the file is not owned by the current user");
|
|
13923
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
|
|
13924
|
+
unsafe("the file exceeds the size limit");
|
|
13925
|
+
const bytes = readFileSync5(fd);
|
|
13926
|
+
const after = fstatSync2(fd);
|
|
13927
|
+
if (!configFileReadsCoherent(before, after)) {
|
|
13928
|
+
unsafe("the file changed while being read");
|
|
13929
|
+
}
|
|
13930
|
+
return parseEnvFile(bytes.toString("utf8"));
|
|
13931
|
+
} finally {
|
|
13932
|
+
if (fd !== -1)
|
|
13933
|
+
closeSync2(fd);
|
|
13934
|
+
}
|
|
13935
|
+
}
|
|
13936
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
13937
|
+
const parsed = readAppConfigFile(path);
|
|
13938
|
+
if (!parsed)
|
|
13939
|
+
return null;
|
|
13940
|
+
for (const key of apiKeyKeys) {
|
|
13941
|
+
if (parsed.unusable.has(key)) {
|
|
13942
|
+
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
13943
|
+
}
|
|
13944
|
+
}
|
|
13945
|
+
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
13946
|
+
if (new Set(values).size > 1) {
|
|
13947
|
+
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
13948
|
+
}
|
|
13949
|
+
return values[0] ?? null;
|
|
13950
|
+
}
|
|
13951
|
+
function appConfigDiskValue(name, env, keys) {
|
|
13952
|
+
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
13953
|
+
if (wanted.length === 0)
|
|
13954
|
+
return null;
|
|
13955
|
+
for (const path of credentialDiskSources(name, env)) {
|
|
13956
|
+
const parsed = readAppConfigFile(path);
|
|
13957
|
+
if (!parsed)
|
|
13958
|
+
continue;
|
|
13959
|
+
if (wanted.some((key) => parsed.unusable.has(key))) {
|
|
13960
|
+
return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
|
|
13961
|
+
}
|
|
13962
|
+
const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
13963
|
+
if (new Set(values).size > 1)
|
|
13964
|
+
throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
|
|
13965
|
+
for (const key of wanted) {
|
|
13966
|
+
if (parsed.unusable.has(key))
|
|
13967
|
+
return { key, value: "", path, unusable: true };
|
|
13968
|
+
const value = parsed.values.get(key)?.trim();
|
|
13969
|
+
if (value)
|
|
13970
|
+
return { key, value, path };
|
|
13971
|
+
}
|
|
13972
|
+
}
|
|
13973
|
+
return null;
|
|
13974
|
+
}
|
|
13975
|
+
function assertUsableCredential(appName, source, value) {
|
|
13976
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
13977
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey2(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
|
|
13978
|
+
}
|
|
13979
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
13980
|
+
return;
|
|
13981
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
|
|
13982
|
+
}
|
|
13983
|
+
function sealCredential(fields) {
|
|
13984
|
+
const { apiKey } = fields;
|
|
13985
|
+
const visible = {
|
|
13986
|
+
tier: fields.tier,
|
|
13987
|
+
source: fields.source,
|
|
13988
|
+
deliberate: fields.deliberate,
|
|
13989
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
13990
|
+
warning: fields.warning
|
|
13991
|
+
};
|
|
13992
|
+
const sealed = { ...visible };
|
|
13993
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
13994
|
+
value: apiKey,
|
|
13995
|
+
enumerable: false,
|
|
13996
|
+
writable: false,
|
|
13997
|
+
configurable: false
|
|
13998
|
+
});
|
|
13999
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
14000
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
14001
|
+
value: fields.pointerVaultKey,
|
|
14002
|
+
enumerable: false,
|
|
14003
|
+
writable: false,
|
|
14004
|
+
configurable: false
|
|
14005
|
+
});
|
|
14006
|
+
}
|
|
14007
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM2, {
|
|
14008
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
14009
|
+
enumerable: false,
|
|
14010
|
+
writable: false,
|
|
14011
|
+
configurable: false
|
|
14012
|
+
});
|
|
14013
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL2, {
|
|
14014
|
+
value: true,
|
|
14015
|
+
enumerable: false,
|
|
14016
|
+
writable: false,
|
|
14017
|
+
configurable: false
|
|
14018
|
+
});
|
|
14019
|
+
return Object.freeze(sealed);
|
|
14020
|
+
}
|
|
14021
|
+
function isSealedCredential(credential) {
|
|
14022
|
+
return credential[CREDENTIAL_SEAL2] === true;
|
|
14023
|
+
}
|
|
14024
|
+
function explicitCredential(appName, apiKey) {
|
|
14025
|
+
const source = "explicit apiKey option";
|
|
14026
|
+
assertUsableCredential(appName, source, apiKey);
|
|
14027
|
+
return sealCredential({
|
|
14028
|
+
apiKey,
|
|
14029
|
+
tier: "argument",
|
|
14030
|
+
source,
|
|
14031
|
+
deliberate: true,
|
|
14032
|
+
diskCandidates: [],
|
|
14033
|
+
warning: null
|
|
14034
|
+
});
|
|
14035
|
+
}
|
|
14036
|
+
function validateAndSealResolvedCredential(appName, credential) {
|
|
14037
|
+
const apiKey = credential.apiKey;
|
|
14038
|
+
assertUsableCredential(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
|
|
14039
|
+
if (!isSealedCredential(credential)) {
|
|
14040
|
+
return sealCredential({
|
|
14041
|
+
apiKey,
|
|
14042
|
+
tier: "argument",
|
|
14043
|
+
source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE,
|
|
14044
|
+
deliberate: true,
|
|
14045
|
+
diskCandidates: [],
|
|
14046
|
+
warning: null
|
|
14047
|
+
});
|
|
14048
|
+
}
|
|
14049
|
+
return sealCredential({
|
|
14050
|
+
apiKey,
|
|
14051
|
+
tier: credential.tier,
|
|
14052
|
+
source: credential.source,
|
|
14053
|
+
deliberate: credential.deliberate,
|
|
14054
|
+
diskCandidates: credential.diskCandidates,
|
|
14055
|
+
warning: credential.warning,
|
|
14056
|
+
...credential.pointerVaultKey !== undefined ? { pointerVaultKey: credential.pointerVaultKey } : {}
|
|
14057
|
+
});
|
|
14058
|
+
}
|
|
14059
|
+
function firstEnvValue(env, keys) {
|
|
14060
|
+
for (const key of keys) {
|
|
14061
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
14062
|
+
continue;
|
|
14063
|
+
const value = env[key]?.trim();
|
|
14064
|
+
if (value)
|
|
14065
|
+
return { key, value };
|
|
14066
|
+
}
|
|
14067
|
+
return null;
|
|
14068
|
+
}
|
|
14069
|
+
function isAmbientEnvironment(env) {
|
|
14070
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT2] === true;
|
|
14071
|
+
}
|
|
14072
|
+
function defaultKeychainRunner(argv) {
|
|
14073
|
+
const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
|
|
14074
|
+
encoding: "utf8",
|
|
14075
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
14076
|
+
timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
|
|
14077
|
+
});
|
|
14078
|
+
return {
|
|
14079
|
+
status: result.status,
|
|
14080
|
+
stdout: result.stdout ?? "",
|
|
14081
|
+
stderr: result.error ? result.error.message : result.stderr ?? ""
|
|
14082
|
+
};
|
|
14083
|
+
}
|
|
14084
|
+
function keychainTierEnabled(env, options) {
|
|
14085
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
14086
|
+
return false;
|
|
14087
|
+
if (options.enabled !== undefined)
|
|
14088
|
+
return options.enabled;
|
|
14089
|
+
return options.run !== undefined || isAmbientEnvironment(env);
|
|
14090
|
+
}
|
|
14091
|
+
function keychainAccount(env, options) {
|
|
14092
|
+
const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
|
|
14093
|
+
if (station)
|
|
14094
|
+
return station;
|
|
14095
|
+
const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
|
|
14096
|
+
if (host)
|
|
14097
|
+
return host;
|
|
14098
|
+
const user = env.USER?.trim();
|
|
14099
|
+
return user || null;
|
|
14100
|
+
}
|
|
14101
|
+
function keychainFailureHint(text) {
|
|
14102
|
+
const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
|
|
14103
|
+
const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
14104
|
+
return clean ? `: ${clean}` : "";
|
|
14105
|
+
}
|
|
14106
|
+
function readKeychainItem(name, env, kind, options) {
|
|
14107
|
+
if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
|
|
14108
|
+
return null;
|
|
14109
|
+
const account = keychainAccount(env, options);
|
|
14110
|
+
if (!account)
|
|
14111
|
+
return null;
|
|
14112
|
+
const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
|
|
14113
|
+
const source = `keychain:${service}@${account}`;
|
|
14114
|
+
const run = options.run ?? defaultKeychainRunner;
|
|
14115
|
+
let result;
|
|
14116
|
+
try {
|
|
14117
|
+
result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
|
|
14118
|
+
} catch (error) {
|
|
14119
|
+
const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
|
|
14120
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
|
|
14121
|
+
}
|
|
14122
|
+
if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
|
|
14123
|
+
return null;
|
|
14124
|
+
if (result.status !== 0) {
|
|
14125
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
|
|
14126
|
+
}
|
|
14127
|
+
const value = result.stdout.trim();
|
|
14128
|
+
if (!value) {
|
|
14129
|
+
throw new CredentialResolutionError(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
|
|
14130
|
+
}
|
|
14131
|
+
return { value, source };
|
|
14132
|
+
}
|
|
14133
|
+
function keychainConfigValue(name, env, options = {}) {
|
|
14134
|
+
return readKeychainItem(name, env, "api-url", options);
|
|
14135
|
+
}
|
|
14136
|
+
function snapshotClientEnvironment(name, env) {
|
|
14137
|
+
const keys = clientTransportEnvKeys2(name);
|
|
14138
|
+
const ambient = isAmbientEnvironment(env);
|
|
14139
|
+
const snapshot = Object.create(null);
|
|
14140
|
+
for (const key of [
|
|
14141
|
+
...keys.apiUrlKeys,
|
|
14142
|
+
...keys.apiKeyKeys,
|
|
14143
|
+
credentialOverrideEnvKey2(name),
|
|
14144
|
+
credentialPointerEnvKey2(name),
|
|
14145
|
+
CREDENTIAL_PROFILE_ENV_KEY2,
|
|
14146
|
+
"HOME",
|
|
14147
|
+
HASNA_HOME_ENV_KEY,
|
|
14148
|
+
HASNA_CONFIG_HOME_ENV_KEY,
|
|
14149
|
+
KEYCHAIN_STATION_ENV_KEY,
|
|
14150
|
+
"USER"
|
|
14151
|
+
]) {
|
|
14152
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
14153
|
+
if (!descriptor)
|
|
14154
|
+
continue;
|
|
14155
|
+
if (!("value" in descriptor)) {
|
|
14156
|
+
throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
|
|
14157
|
+
}
|
|
14158
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
14159
|
+
throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
|
|
14160
|
+
}
|
|
14161
|
+
snapshot[key] = descriptor.value;
|
|
14162
|
+
}
|
|
14163
|
+
if (ambient) {
|
|
14164
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT2, {
|
|
14165
|
+
value: true,
|
|
14166
|
+
enumerable: false,
|
|
14167
|
+
writable: false,
|
|
14168
|
+
configurable: false
|
|
14169
|
+
});
|
|
14170
|
+
}
|
|
14171
|
+
return Object.freeze(snapshot);
|
|
14172
|
+
}
|
|
14173
|
+
function resolveCredential(name, env, options = {}) {
|
|
14174
|
+
env = snapshotClientEnvironment(name, env);
|
|
14175
|
+
const { apiKeyKeys } = clientTransportEnvKeys2(name);
|
|
14176
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
14177
|
+
if (options.apiKey !== undefined) {
|
|
14178
|
+
const explicitKey = options.apiKey.trim();
|
|
14179
|
+
if (!explicitKey) {
|
|
14180
|
+
throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
|
|
14181
|
+
}
|
|
14182
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
14183
|
+
return sealCredential({
|
|
14184
|
+
apiKey: explicitKey,
|
|
14185
|
+
tier: "argument",
|
|
14186
|
+
source: "explicit apiKey argument",
|
|
14187
|
+
deliberate: true,
|
|
14188
|
+
diskCandidates: diskPaths,
|
|
14189
|
+
warning: null
|
|
14190
|
+
});
|
|
14191
|
+
}
|
|
14192
|
+
const overrideKeyName = credentialOverrideEnvKey2(name);
|
|
14193
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
14194
|
+
if (overrideRaw !== undefined) {
|
|
14195
|
+
const override = overrideRaw.trim();
|
|
14196
|
+
if (!override) {
|
|
14197
|
+
throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
|
|
14198
|
+
}
|
|
14199
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
14200
|
+
return sealCredential({
|
|
14201
|
+
apiKey: override,
|
|
14202
|
+
tier: "override",
|
|
14203
|
+
source: overrideKeyName,
|
|
14204
|
+
deliberate: true,
|
|
14205
|
+
diskCandidates: diskPaths,
|
|
14206
|
+
warning: null
|
|
14207
|
+
});
|
|
14208
|
+
}
|
|
14209
|
+
const pointerKeyName = credentialPointerEnvKey2(name);
|
|
14210
|
+
const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
|
|
14211
|
+
if (pointerRaw !== undefined) {
|
|
14212
|
+
const pointer = pointerRaw.trim();
|
|
14213
|
+
if (!pointer) {
|
|
14214
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
|
|
14215
|
+
}
|
|
14216
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
14217
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
|
|
14218
|
+
}
|
|
14219
|
+
return sealCredential({
|
|
14220
|
+
apiKey: "",
|
|
14221
|
+
pointerVaultKey: pointer,
|
|
14222
|
+
tier: "pointer",
|
|
14223
|
+
source: pointerKeyName,
|
|
14224
|
+
deliberate: true,
|
|
14225
|
+
diskCandidates: diskPaths,
|
|
14226
|
+
warning: null
|
|
14227
|
+
});
|
|
14228
|
+
}
|
|
14229
|
+
if (options.profile !== undefined && !options.profile.trim()) {
|
|
14230
|
+
throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
|
|
14231
|
+
}
|
|
14232
|
+
const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY2) ? env[CREDENTIAL_PROFILE_ENV_KEY2] : undefined;
|
|
14233
|
+
if (profileRaw !== undefined && !profileRaw.trim()) {
|
|
14234
|
+
throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY2} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY2]);
|
|
14235
|
+
}
|
|
14236
|
+
const profile = options.profile?.trim() || profileRaw?.trim();
|
|
14237
|
+
if (profile) {
|
|
14238
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY2;
|
|
14239
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
14240
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
14241
|
+
}
|
|
14242
|
+
const paths = profileDiskSources(name, env, profile);
|
|
14243
|
+
for (const path of paths) {
|
|
14244
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
14245
|
+
if (value) {
|
|
14246
|
+
assertUsableCredential(name, path, value);
|
|
14247
|
+
return sealCredential({
|
|
14248
|
+
apiKey: value,
|
|
14249
|
+
tier: "profile",
|
|
14250
|
+
source: path,
|
|
14251
|
+
deliberate: true,
|
|
14252
|
+
diskCandidates: paths,
|
|
14253
|
+
warning: null
|
|
14254
|
+
});
|
|
14255
|
+
}
|
|
14256
|
+
}
|
|
14257
|
+
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY2}.`, paths);
|
|
14258
|
+
}
|
|
14259
|
+
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
14260
|
+
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
14261
|
+
if (blankEnv) {
|
|
14262
|
+
throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
|
|
14263
|
+
}
|
|
14264
|
+
if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
|
|
14265
|
+
throw new CredentialResolutionError(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
|
|
14266
|
+
}
|
|
14267
|
+
const envHit = firstEnvValue(env, apiKeyKeys);
|
|
14268
|
+
const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
|
|
14269
|
+
if (keychainHit) {
|
|
14270
|
+
assertUsableCredential(name, keychainHit.source, keychainHit.value);
|
|
14271
|
+
const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain is re-read on every call while ` + `an environment variable is a snapshot. Reconcile them \u2014 a rotation that updated only one leaves ` + `the other to fail 401 wherever it is loaded first.` : null;
|
|
14272
|
+
return sealCredential({
|
|
14273
|
+
apiKey: keychainHit.value,
|
|
14274
|
+
tier: "keychain",
|
|
14275
|
+
source: keychainHit.source,
|
|
14276
|
+
deliberate: false,
|
|
14277
|
+
diskCandidates: diskPaths,
|
|
14278
|
+
warning
|
|
14279
|
+
});
|
|
14280
|
+
}
|
|
14281
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
14282
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
14283
|
+
if (diskHits.length > 0) {
|
|
14284
|
+
const winner = diskHits[0];
|
|
14285
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
14286
|
+
const divergentSources = [
|
|
14287
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
14288
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
14289
|
+
];
|
|
14290
|
+
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
|
|
14291
|
+
return sealCredential({
|
|
14292
|
+
apiKey: winner.value,
|
|
14293
|
+
tier: winner.src.tier,
|
|
14294
|
+
source: winner.src.path,
|
|
14295
|
+
deliberate: false,
|
|
14296
|
+
diskCandidates: diskPaths,
|
|
14297
|
+
warning
|
|
14298
|
+
});
|
|
14299
|
+
}
|
|
14300
|
+
if (envHit) {
|
|
14301
|
+
assertUsableCredential(name, envHit.key, envHit.value);
|
|
14302
|
+
return sealCredential({
|
|
14303
|
+
apiKey: envHit.value,
|
|
14304
|
+
tier: "env",
|
|
14305
|
+
source: envHit.key,
|
|
14306
|
+
deliberate: false,
|
|
14307
|
+
diskCandidates: diskPaths,
|
|
14308
|
+
warning: null
|
|
14309
|
+
});
|
|
14310
|
+
}
|
|
14311
|
+
return null;
|
|
14312
|
+
}
|
|
14313
|
+
async function completePointerCredential(name, pointerResolution, env = process.env) {
|
|
14314
|
+
const vaultKey = pointerResolution.pointerVaultKey;
|
|
14315
|
+
const pointerEnvKey = pointerResolution.source;
|
|
14316
|
+
if (!vaultKey) {
|
|
14317
|
+
throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
|
|
14318
|
+
}
|
|
14319
|
+
let secretsSdk;
|
|
14320
|
+
try {
|
|
14321
|
+
secretsSdk = requireSecretsSdk2(SECRETS_PACKAGE_SPECIFIER2);
|
|
14322
|
+
} catch {
|
|
14323
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
|
|
14324
|
+
}
|
|
14325
|
+
let client;
|
|
14326
|
+
try {
|
|
14327
|
+
client = secretsSdk.createSecretsClientFromEnv(env);
|
|
14328
|
+
} catch {
|
|
14329
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
14330
|
+
}
|
|
14331
|
+
let secret;
|
|
14332
|
+
try {
|
|
14333
|
+
secret = await client.getSecret({ key: vaultKey });
|
|
14334
|
+
} catch {
|
|
14335
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
14336
|
+
}
|
|
14337
|
+
const value = secret.value;
|
|
14338
|
+
if (!value) {
|
|
14339
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
|
|
14340
|
+
}
|
|
14341
|
+
assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
|
|
14342
|
+
return sealCredential({
|
|
14343
|
+
apiKey: value,
|
|
14344
|
+
tier: "pointer",
|
|
14345
|
+
source: `${pointerEnvKey} -> vault:${vaultKey}`,
|
|
14346
|
+
deliberate: true,
|
|
14347
|
+
diskCandidates: pointerResolution.diskCandidates,
|
|
14348
|
+
warning: null
|
|
14349
|
+
});
|
|
14350
|
+
}
|
|
14351
|
+
function defaultFleetGatewayBaseUrl(name) {
|
|
14352
|
+
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
14353
|
+
}
|
|
14354
|
+
function isValidDnsDomain(value) {
|
|
14355
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
14356
|
+
return false;
|
|
14357
|
+
}
|
|
14358
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
14359
|
+
}
|
|
14360
|
+
function validateAppSlug(name) {
|
|
14361
|
+
if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
|
|
14362
|
+
throw new Error("App name must be one lowercase DNS label.");
|
|
14363
|
+
}
|
|
14364
|
+
return name;
|
|
14365
|
+
}
|
|
14366
|
+
function rawAuthority(value) {
|
|
14367
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
14368
|
+
if (!match)
|
|
14369
|
+
throw new Error("API URL must be absolute.");
|
|
14370
|
+
const afterScheme = value.slice(match[0].length);
|
|
14371
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
14372
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
14373
|
+
if (!authority)
|
|
14374
|
+
throw new Error("API URL must include a hostname.");
|
|
14375
|
+
return authority;
|
|
14376
|
+
}
|
|
14377
|
+
function assertCanonicalPort(port) {
|
|
14378
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
14379
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
14380
|
+
}
|
|
14381
|
+
const numericPort = Number(port);
|
|
14382
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
14383
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
14384
|
+
}
|
|
14385
|
+
}
|
|
14386
|
+
function canonicalAuthorityHostname(authority) {
|
|
14387
|
+
let rawHostname;
|
|
14388
|
+
if (authority.startsWith("[")) {
|
|
14389
|
+
const closingBracket = authority.indexOf("]");
|
|
14390
|
+
if (closingBracket === -1) {
|
|
14391
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
14392
|
+
}
|
|
14393
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
14394
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
14395
|
+
if (portSuffix) {
|
|
14396
|
+
if (!portSuffix.startsWith(":")) {
|
|
14397
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
14398
|
+
}
|
|
14399
|
+
assertCanonicalPort(portSuffix.slice(1));
|
|
14400
|
+
}
|
|
14401
|
+
if (isIP2(rawHostname.slice(1, -1)) !== 6) {
|
|
14402
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
14403
|
+
}
|
|
14404
|
+
} else {
|
|
14405
|
+
const firstColon = authority.indexOf(":");
|
|
14406
|
+
const lastColon = authority.lastIndexOf(":");
|
|
14407
|
+
if (firstColon !== lastColon) {
|
|
14408
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
14409
|
+
}
|
|
14410
|
+
if (lastColon !== -1) {
|
|
14411
|
+
const port = authority.slice(lastColon + 1);
|
|
14412
|
+
assertCanonicalPort(port);
|
|
14413
|
+
rawHostname = authority.slice(0, lastColon);
|
|
14414
|
+
} else {
|
|
14415
|
+
rawHostname = authority;
|
|
14416
|
+
}
|
|
14417
|
+
const ipVersion = isIP2(rawHostname);
|
|
14418
|
+
const numericAddressParts = rawHostname.split(".");
|
|
14419
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
14420
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
|
|
14421
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
14422
|
+
}
|
|
14423
|
+
}
|
|
14424
|
+
return rawHostname.toLowerCase();
|
|
14425
|
+
}
|
|
14426
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
14427
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
14428
|
+
}
|
|
14429
|
+
function toV1BaseUrl(apiUrl) {
|
|
14430
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
14431
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
14432
|
+
}
|
|
14433
|
+
const input = apiUrl.trim();
|
|
14434
|
+
const authority = rawAuthority(input);
|
|
14435
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
14436
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
14437
|
+
}
|
|
14438
|
+
const canonicalHostname = canonicalAuthorityHostname(authority);
|
|
14439
|
+
const url = new URL(input);
|
|
14440
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
14441
|
+
throw new Error("API URL must use http or https.");
|
|
14442
|
+
}
|
|
14443
|
+
if (url.username || url.password) {
|
|
14444
|
+
throw new Error("API URL must not include credentials.");
|
|
14445
|
+
}
|
|
14446
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
14447
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
14448
|
+
}
|
|
14449
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
14450
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
14451
|
+
}
|
|
14452
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
14453
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
14454
|
+
}
|
|
14455
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
|
|
14456
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
14457
|
+
}
|
|
14458
|
+
if (url.search || url.hash) {
|
|
14459
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
14460
|
+
}
|
|
14461
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
14462
|
+
if (path.endsWith("/v1"))
|
|
14463
|
+
path = path.slice(0, -"/v1".length);
|
|
14464
|
+
url.pathname = `${path}/v1`;
|
|
14465
|
+
return url.toString().replace(/\/+$/, "");
|
|
14466
|
+
}
|
|
14467
|
+
function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
|
|
14468
|
+
env = snapshotClientEnvironment(name, env);
|
|
14469
|
+
const keys = clientTransportEnvKeys2(name);
|
|
14470
|
+
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
14471
|
+
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
14472
|
+
if (blankUrl) {
|
|
14473
|
+
throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
|
|
14474
|
+
}
|
|
14475
|
+
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
|
|
14476
|
+
if (controlledUrl) {
|
|
14477
|
+
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
14478
|
+
}
|
|
14479
|
+
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
14480
|
+
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
14481
|
+
throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
|
|
14482
|
+
}
|
|
14483
|
+
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
14484
|
+
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
14485
|
+
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
14486
|
+
if (diskConfigUrlHit?.unusable) {
|
|
14487
|
+
throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
|
|
14488
|
+
}
|
|
14489
|
+
const urlCandidates = [
|
|
14490
|
+
...envUrlHit ? [envUrlHit] : [],
|
|
14491
|
+
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
14492
|
+
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
14493
|
+
];
|
|
14494
|
+
const configuredUrl = urlCandidates[0] ?? null;
|
|
14495
|
+
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
14496
|
+
if (configuredUrl && divergentUrls.length > 0) {
|
|
14497
|
+
throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
|
|
14498
|
+
}
|
|
14499
|
+
const warnings = [];
|
|
14500
|
+
if (configuredUrl && !envUrlHit) {
|
|
14501
|
+
warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
|
|
14502
|
+
}
|
|
14503
|
+
const credential = resolveCredential(name, env, options.credentials);
|
|
14504
|
+
if (!credential) {
|
|
14505
|
+
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
14506
|
+
const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
|
|
14507
|
+
warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
|
|
14508
|
+
throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
14509
|
+
}
|
|
14510
|
+
if (credential.warning)
|
|
14511
|
+
warnings.push(credential.warning);
|
|
14512
|
+
let urlHit;
|
|
14513
|
+
if (configuredUrl) {
|
|
14514
|
+
urlHit = configuredUrl;
|
|
14515
|
+
} else {
|
|
14516
|
+
try {
|
|
14517
|
+
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
14518
|
+
} catch (error) {
|
|
14519
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14520
|
+
throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
|
|
14521
|
+
}
|
|
14522
|
+
}
|
|
14523
|
+
const apiUrlSource = urlHit.key;
|
|
14524
|
+
let baseUrl;
|
|
14525
|
+
try {
|
|
14526
|
+
baseUrl = toV1BaseUrl(urlHit.value);
|
|
14527
|
+
} catch (error) {
|
|
14528
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14529
|
+
throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
14530
|
+
}
|
|
14531
|
+
return {
|
|
14532
|
+
resolution: {
|
|
14533
|
+
transport: "http",
|
|
14534
|
+
transportSource: urlHit.key,
|
|
14535
|
+
baseUrl,
|
|
14536
|
+
apiUrlSource,
|
|
14537
|
+
apiKeyPresent: true,
|
|
14538
|
+
apiKeySource: credential.source,
|
|
14539
|
+
apiKeyTier: credential.tier,
|
|
14540
|
+
misconfigured: false,
|
|
14541
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
14542
|
+
},
|
|
14543
|
+
credential
|
|
14544
|
+
};
|
|
14545
|
+
}
|
|
14546
|
+
function credentialDiskSourcesForMessage(name, env) {
|
|
14547
|
+
const paths = credentialDiskSources(name, env);
|
|
14548
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
14549
|
+
}
|
|
14550
|
+
function currentCredential(name, apiKey) {
|
|
14551
|
+
if (typeof apiKey === "function") {
|
|
14552
|
+
return validateAndSealResolvedCredential(name, apiKey());
|
|
14553
|
+
}
|
|
14554
|
+
return explicitCredential(name, apiKey);
|
|
14555
|
+
}
|
|
14556
|
+
async function resolveRequestCredential(name, apiKey, env = process.env) {
|
|
14557
|
+
const resolved = currentCredential(name, apiKey);
|
|
14558
|
+
if (resolved.tier === "pointer") {
|
|
14559
|
+
return completePointerCredential(name, resolved, env);
|
|
14560
|
+
}
|
|
14561
|
+
return resolved;
|
|
14562
|
+
}
|
|
14563
|
+
function authFailureGuidance(credential) {
|
|
14564
|
+
const origin = `The API key for this request came from ${credential.source}`;
|
|
14565
|
+
if (credential.deliberate) {
|
|
14566
|
+
const remedy = credential.source === CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE ? `Fix that provider so it returns the current key, or replace it with resolveCredential() ` + `so diagnostics can name the original source.` : `Rotate that key, or unset the override to use the credential on disk.`;
|
|
14567
|
+
return `${origin} \u2014 a credential you selected deliberately. It was NOT substituted with any other key: ` + `falling back here would authenticate as a different principal than the one you named, which is ` + `exactly the failure an override exists to prevent. ${remedy}`;
|
|
14568
|
+
}
|
|
14569
|
+
if (credential.tier === "env") {
|
|
14570
|
+
const target = credential.diskCandidates[0];
|
|
14571
|
+
const remedy = target ? `Store the CURRENT key in the Keychain or write it to ${target} \u2014 both are re-read on every call, so ` + `rotations take effect immediately and in every shell. Do not simply unset ${credential.source}: ` + `nothing was found in the Keychain or on disk, so that would leave this client with no credential at all.` : `This environment has no HOME or HASNA_HOME, so no credential file could be consulted; the disk tier is ` + `unavailable here and there is nothing to fall back to. Set HOME, or supply the key explicitly.`;
|
|
14572
|
+
return `${origin}, a variable in this process's environment. If a wrapper injected it for this one process, the ` + `wrapper re-reads its store on every invocation and the stored key itself is being rejected \u2014 rotate it. ` + `If this SHELL exported it, the export is a snapshot taken when the shell started: a STALE SHELL that ` + `exported the key before it was rotated keeps sending the old one until it exits. ${remedy}`;
|
|
14573
|
+
}
|
|
14574
|
+
if (credential.tier === "keychain") {
|
|
14575
|
+
return `${origin}, which was re-read from the Keychain on this very call \u2014 so a stale shell is NOT the cause ` + `here. The stored item is genuinely being rejected: update it with the current key, or re-run the fleet ` + `key distribution so this machine gets the current key.`;
|
|
14576
|
+
}
|
|
14577
|
+
return `${origin}, which was re-read from disk on this very call \u2014 so a stale shell is NOT the cause here. ` + `The stored credential is genuinely being rejected: rotate it, or re-run the fleet key distribution ` + `so this machine gets the current key.`;
|
|
14578
|
+
}
|
|
14579
|
+
function assertNoAuthorityOverrideHeaders(headers, source) {
|
|
14580
|
+
if (!headers)
|
|
14581
|
+
return;
|
|
14582
|
+
const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS2.has(name.trim().toLowerCase()));
|
|
14583
|
+
if (forbidden) {
|
|
14584
|
+
throw new Error(`Authenticated ${source} headers must not set authority header '${forbidden}'.`);
|
|
14585
|
+
}
|
|
14586
|
+
}
|
|
14587
|
+
function appendQuery(path, query) {
|
|
14588
|
+
if (!query)
|
|
14589
|
+
return path;
|
|
14590
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams;
|
|
14591
|
+
if (!(query instanceof URLSearchParams)) {
|
|
14592
|
+
for (const [key, value] of Object.entries(query)) {
|
|
14593
|
+
if (value === null || value === undefined)
|
|
14594
|
+
continue;
|
|
14595
|
+
if (Array.isArray(value)) {
|
|
14596
|
+
for (const v of value)
|
|
14597
|
+
params.append(key, String(v));
|
|
14598
|
+
} else {
|
|
14599
|
+
params.append(key, String(value));
|
|
14600
|
+
}
|
|
14601
|
+
}
|
|
14602
|
+
}
|
|
14603
|
+
const qs = params.toString();
|
|
14604
|
+
if (!qs)
|
|
14605
|
+
return path;
|
|
14606
|
+
return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
|
|
14607
|
+
}
|
|
14608
|
+
function createHasnaHttpTransportInternal(options, requestBindingProvider) {
|
|
14609
|
+
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
14610
|
+
const base = toV1BaseUrl(options.baseUrl);
|
|
14611
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
14612
|
+
const sleep = options.sleepImpl ?? defaultSleep;
|
|
14613
|
+
const defaultRetry = options.retry;
|
|
14614
|
+
function resolveRetry(callRetry) {
|
|
14615
|
+
const chosen = callRetry !== undefined ? callRetry : defaultRetry;
|
|
14616
|
+
if (chosen === false)
|
|
14617
|
+
return null;
|
|
14618
|
+
const r = chosen ?? {};
|
|
14619
|
+
return {
|
|
14620
|
+
retries: r.retries ?? 2,
|
|
14621
|
+
baseDelayMs: r.baseDelayMs ?? 200,
|
|
14622
|
+
maxDelayMs: r.maxDelayMs ?? 2000,
|
|
14623
|
+
retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
|
|
14624
|
+
};
|
|
14625
|
+
}
|
|
14626
|
+
async function once(method, rel, url, body, opts, credential) {
|
|
14627
|
+
assertNoAuthorityOverrideHeaders(options.headers, "transport");
|
|
14628
|
+
assertNoAuthorityOverrideHeaders(opts.headers, "request");
|
|
14629
|
+
const headers = {
|
|
14630
|
+
"x-api-key": credential.apiKey,
|
|
14631
|
+
Authorization: `Bearer ${credential.apiKey}`,
|
|
14632
|
+
Accept: "application/json",
|
|
14633
|
+
...options.headers ?? {},
|
|
14634
|
+
...opts.headers ?? {}
|
|
14635
|
+
};
|
|
14636
|
+
if (opts.idempotencyKey)
|
|
14637
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
14638
|
+
const init = {
|
|
14639
|
+
method,
|
|
14640
|
+
headers,
|
|
14641
|
+
redirect: "manual"
|
|
14642
|
+
};
|
|
14643
|
+
if (body !== undefined) {
|
|
14644
|
+
headers["Content-Type"] = "application/json";
|
|
14645
|
+
init.body = JSON.stringify(body);
|
|
14646
|
+
}
|
|
14647
|
+
const controller = new AbortController;
|
|
14648
|
+
const onAbort = () => controller.abort();
|
|
14649
|
+
if (opts.signal) {
|
|
14650
|
+
if (opts.signal.aborted)
|
|
14651
|
+
controller.abort();
|
|
14652
|
+
else
|
|
14653
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
14654
|
+
}
|
|
14655
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
|
|
14656
|
+
init.signal = controller.signal;
|
|
14657
|
+
let response;
|
|
14658
|
+
try {
|
|
14659
|
+
response = await fetchImpl(url, init);
|
|
14660
|
+
} catch (error) {
|
|
14661
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
14662
|
+
if (opts.signal?.aborted)
|
|
14663
|
+
return { ok: false, retryable: false, error: err };
|
|
14664
|
+
return { ok: false, retryable: true, error: err };
|
|
14665
|
+
} finally {
|
|
14666
|
+
clearTimeout(timer);
|
|
14667
|
+
if (opts.signal)
|
|
14668
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
14669
|
+
}
|
|
14670
|
+
const authenticationFailure = response.status === 401 || response.status === 403;
|
|
14671
|
+
let parsed = undefined;
|
|
14672
|
+
if (authenticationFailure) {
|
|
14673
|
+
try {
|
|
14674
|
+
await response.body?.cancel();
|
|
14675
|
+
} catch {}
|
|
14676
|
+
} else {
|
|
14677
|
+
const text = await response.text();
|
|
14678
|
+
if (text.length > 0) {
|
|
14679
|
+
try {
|
|
14680
|
+
parsed = JSON.parse(text);
|
|
14681
|
+
} catch {
|
|
14682
|
+
parsed = text;
|
|
14683
|
+
}
|
|
14684
|
+
}
|
|
14685
|
+
}
|
|
14686
|
+
if (!response.ok) {
|
|
14687
|
+
if (response.status >= 300 && response.status < 400) {
|
|
14688
|
+
return {
|
|
14689
|
+
ok: false,
|
|
14690
|
+
retryable: false,
|
|
14691
|
+
error: new HasnaHttpError(method, rel, response.status, parsed)
|
|
14692
|
+
};
|
|
14693
|
+
}
|
|
14694
|
+
if (authenticationFailure) {
|
|
14695
|
+
return {
|
|
14696
|
+
ok: false,
|
|
14697
|
+
retryable: false,
|
|
14698
|
+
error: new HasnaHttpError(method, rel, response.status, undefined, {
|
|
14699
|
+
source: credential.source,
|
|
14700
|
+
tier: credential.tier,
|
|
14701
|
+
guidance: authFailureGuidance(credential)
|
|
14702
|
+
})
|
|
14703
|
+
};
|
|
14704
|
+
}
|
|
14705
|
+
const retry = resolveRetry(opts.retry);
|
|
14706
|
+
const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
|
|
14707
|
+
return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
|
|
14708
|
+
}
|
|
14709
|
+
return { ok: true, value: parsed };
|
|
14710
|
+
}
|
|
14711
|
+
async function request(method, path, body, opts = {}) {
|
|
14712
|
+
const upper = method.toUpperCase();
|
|
14713
|
+
const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
|
|
14714
|
+
const retry = resolveRetry(opts.retry);
|
|
14715
|
+
const methodRetryable = IDEMPOTENT_METHODS2.has(upper) || Boolean(opts.idempotencyKey);
|
|
14716
|
+
const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
|
|
14717
|
+
const binding = requestBindingProvider ? await requestBindingProvider() : {
|
|
14718
|
+
baseUrl: base,
|
|
14719
|
+
credential: await resolveRequestCredential(options.name, options.apiKey)
|
|
14720
|
+
};
|
|
14721
|
+
const url = `${binding.baseUrl}${rel}`;
|
|
14722
|
+
const credential = binding.credential;
|
|
14723
|
+
let last = null;
|
|
14724
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
14725
|
+
const result = await once(upper, rel, url, body, opts, credential);
|
|
14726
|
+
if (result.ok)
|
|
14727
|
+
return result.value;
|
|
14728
|
+
last = result;
|
|
14729
|
+
const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
|
|
14730
|
+
if (!canRetry)
|
|
14731
|
+
break;
|
|
14732
|
+
const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
|
|
14733
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
14734
|
+
await sleep(backoff + jitter);
|
|
14735
|
+
}
|
|
14736
|
+
throw last.error;
|
|
14737
|
+
}
|
|
14738
|
+
return {
|
|
14739
|
+
baseUrl: base,
|
|
14740
|
+
request,
|
|
14741
|
+
get: (path, opts) => request("GET", path, undefined, opts),
|
|
14742
|
+
post: (path, body, opts) => request("POST", path, body, opts),
|
|
14743
|
+
put: (path, body, opts) => request("PUT", path, body, opts),
|
|
14744
|
+
patch: (path, body, opts) => request("PATCH", path, body, opts),
|
|
14745
|
+
del: (path, body, opts) => request("DELETE", path, body, opts)
|
|
14746
|
+
};
|
|
14747
|
+
}
|
|
14748
|
+
function createClientTransport(name, env = process.env, overrides) {
|
|
14749
|
+
const credentialOptions = overrides?.credentials;
|
|
14750
|
+
const snapshotOptions = { ...credentialOptions ? { credentials: credentialOptions } : {} };
|
|
14751
|
+
const resolution = resolveClientTransportSnapshot(name, env, snapshotOptions).resolution;
|
|
14752
|
+
const sameBinding = (left, right) => left.resolution.baseUrl === right.resolution.baseUrl && left.credential.apiKey === right.credential.apiKey && left.credential.pointerVaultKey === right.credential.pointerVaultKey && left.credential.source === right.credential.source && left.credential.tier === right.credential.tier;
|
|
14753
|
+
const unstableConfiguration = () => new ClientTransportConfigurationError(name, "The configured service authority or credential changed while a request was being prepared; no authenticated request was sent.");
|
|
14754
|
+
const requestBindingProvider = async () => {
|
|
14755
|
+
const first = resolveClientTransportSnapshot(name, env, snapshotOptions);
|
|
14756
|
+
const reviewed = resolveClientTransportSnapshot(name, env, snapshotOptions);
|
|
14757
|
+
if (!sameBinding(first, reviewed))
|
|
14758
|
+
throw unstableConfiguration();
|
|
14759
|
+
if (reviewed.resolution.baseUrl !== resolution.baseUrl) {
|
|
14760
|
+
throw new ClientTransportConfigurationError(name, "The configured service authority changed; rebuild the client before sending credentials.");
|
|
14761
|
+
}
|
|
14762
|
+
const credential = await resolveRequestCredential(name, () => reviewed.credential, env);
|
|
14763
|
+
const immediatelyBeforeDispatch = resolveClientTransportSnapshot(name, env, snapshotOptions);
|
|
14764
|
+
if (!sameBinding(reviewed, immediatelyBeforeDispatch))
|
|
14765
|
+
throw unstableConfiguration();
|
|
14766
|
+
if (immediatelyBeforeDispatch.resolution.baseUrl !== resolution.baseUrl) {
|
|
14767
|
+
throw new ClientTransportConfigurationError(name, "The configured service authority changed; rebuild the client before sending credentials.");
|
|
14768
|
+
}
|
|
14769
|
+
return { baseUrl: immediatelyBeforeDispatch.resolution.baseUrl, credential };
|
|
14770
|
+
};
|
|
14771
|
+
return {
|
|
14772
|
+
transport: "http",
|
|
14773
|
+
client: createHasnaHttpTransportInternal({
|
|
14774
|
+
name,
|
|
14775
|
+
baseUrl: resolution.baseUrl,
|
|
14776
|
+
apiKey: () => {
|
|
14777
|
+
throw new Error("The authenticated request binding provider was not invoked.");
|
|
14778
|
+
},
|
|
14779
|
+
...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
14780
|
+
...overrides?.headers ? { headers: overrides.headers } : {},
|
|
14781
|
+
...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
14782
|
+
...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
|
|
14783
|
+
...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
14784
|
+
}, requestBindingProvider),
|
|
14785
|
+
resolution
|
|
14786
|
+
};
|
|
14787
|
+
}
|
|
14788
|
+
function resourcePath(resource) {
|
|
14789
|
+
const trimmed = resource.replace(/^\/+|\/+$/g, "");
|
|
14790
|
+
if (!trimmed)
|
|
14791
|
+
throw new Error("resource must be a non-empty path segment");
|
|
14792
|
+
return `/${trimmed}`;
|
|
14793
|
+
}
|
|
14794
|
+
function entityPath(resource, id) {
|
|
14795
|
+
if (id === undefined || id === null || `${id}`.length === 0) {
|
|
14796
|
+
throw new Error("id must be a non-empty string");
|
|
14797
|
+
}
|
|
14798
|
+
return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
|
|
14799
|
+
}
|
|
14800
|
+
function newIdempotencyKey() {
|
|
14801
|
+
const g = globalThis;
|
|
14802
|
+
if (g.crypto?.randomUUID)
|
|
14803
|
+
return g.crypto.randomUUID();
|
|
14804
|
+
return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
14805
|
+
}
|
|
14806
|
+
function extractItems(raw) {
|
|
14807
|
+
if (Array.isArray(raw))
|
|
14808
|
+
return raw;
|
|
14809
|
+
if (raw && typeof raw === "object") {
|
|
14810
|
+
const obj = raw;
|
|
14811
|
+
for (const key of ["items", "data", "results", "rows", "records"]) {
|
|
14812
|
+
if (Array.isArray(obj[key]))
|
|
14813
|
+
return obj[key];
|
|
14814
|
+
}
|
|
14815
|
+
}
|
|
14816
|
+
return [];
|
|
14817
|
+
}
|
|
14818
|
+
function extractTotal(raw) {
|
|
14819
|
+
if (raw && typeof raw === "object") {
|
|
14820
|
+
const obj = raw;
|
|
14821
|
+
for (const key of ["total", "count", "totalCount", "total_count"]) {
|
|
14822
|
+
if (typeof obj[key] === "number")
|
|
14823
|
+
return obj[key];
|
|
14824
|
+
}
|
|
14825
|
+
}
|
|
14826
|
+
return null;
|
|
14827
|
+
}
|
|
14828
|
+
function extractCursor(raw) {
|
|
14829
|
+
if (raw && typeof raw === "object") {
|
|
14830
|
+
const obj = raw;
|
|
14831
|
+
for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
|
|
14832
|
+
if (typeof obj[key] === "string")
|
|
14833
|
+
return obj[key];
|
|
14834
|
+
}
|
|
14835
|
+
}
|
|
14836
|
+
return null;
|
|
14837
|
+
}
|
|
14838
|
+
function isNotFoundHttpError(error) {
|
|
14839
|
+
return typeof error === "object" && error !== null && error.name === "HasnaHttpError" && error.status === 404;
|
|
14840
|
+
}
|
|
14841
|
+
function createHasnaStorageClient(name, transport) {
|
|
14842
|
+
return {
|
|
14843
|
+
name,
|
|
14844
|
+
baseUrl: transport.baseUrl,
|
|
14845
|
+
transport,
|
|
14846
|
+
async list(resource, options = {}) {
|
|
14847
|
+
const raw = await transport.get(resourcePath(resource), options);
|
|
14848
|
+
return {
|
|
14849
|
+
items: extractItems(raw),
|
|
14850
|
+
total: extractTotal(raw),
|
|
14851
|
+
cursor: extractCursor(raw),
|
|
14852
|
+
raw
|
|
14853
|
+
};
|
|
14854
|
+
},
|
|
14855
|
+
async get(resource, id, options = {}) {
|
|
14856
|
+
try {
|
|
14857
|
+
return await transport.get(entityPath(resource, id), options);
|
|
14858
|
+
} catch (error) {
|
|
14859
|
+
if (isNotFoundHttpError(error))
|
|
14860
|
+
return null;
|
|
14861
|
+
throw error;
|
|
14862
|
+
}
|
|
14863
|
+
},
|
|
14864
|
+
async create(resource, body, options = {}) {
|
|
14865
|
+
const { idempotencyKey, ...rest } = options;
|
|
14866
|
+
return transport.post(resourcePath(resource), body, {
|
|
14867
|
+
...rest,
|
|
14868
|
+
idempotencyKey: idempotencyKey ?? newIdempotencyKey()
|
|
14869
|
+
});
|
|
14870
|
+
},
|
|
14871
|
+
async update(resource, id, patch, options = {}) {
|
|
14872
|
+
const { method = "PATCH", idempotencyKey, ...rest } = options;
|
|
14873
|
+
const call = method === "PUT" ? transport.put : transport.patch;
|
|
14874
|
+
return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
|
|
14875
|
+
},
|
|
14876
|
+
async delete(resource, id, options = {}) {
|
|
14877
|
+
try {
|
|
14878
|
+
await transport.del(entityPath(resource, id), undefined, options);
|
|
14879
|
+
} catch (error) {
|
|
14880
|
+
if (isNotFoundHttpError(error))
|
|
14881
|
+
return;
|
|
14882
|
+
throw error;
|
|
14883
|
+
}
|
|
14884
|
+
}
|
|
14885
|
+
};
|
|
14886
|
+
}
|
|
14887
|
+
function resolveStorageClient(name, env = process.env, overrides) {
|
|
14888
|
+
const wired = createClientTransport(name, env, overrides);
|
|
14889
|
+
return { transport: "http", client: createHasnaStorageClient(name, wired.client) };
|
|
14890
|
+
}
|
|
14891
|
+
var CREDENTIAL_PROFILE_ENV_KEY2 = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider", AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER2, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, HasnaHttpError, DEFAULT_RETRY_STATUSES, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2, defaultSleep = (ms) => new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
14892
|
+
var init_storage = __esm(() => {
|
|
14893
|
+
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
14894
|
+
appName;
|
|
14895
|
+
attempted;
|
|
14896
|
+
constructor(appName, message, attempted) {
|
|
14897
|
+
super(message);
|
|
14898
|
+
this.name = "CredentialResolutionError";
|
|
14899
|
+
this.appName = appName;
|
|
14900
|
+
this.attempted = attempted;
|
|
14901
|
+
}
|
|
14902
|
+
};
|
|
14903
|
+
CredentialFileUnsafeError = class CredentialFileUnsafeError extends Error {
|
|
14904
|
+
path;
|
|
14905
|
+
constructor(path, reason) {
|
|
14906
|
+
super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
|
|
14907
|
+
this.name = "CredentialFileUnsafeError";
|
|
14908
|
+
this.path = path;
|
|
14909
|
+
}
|
|
14910
|
+
};
|
|
14911
|
+
MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
|
|
14912
|
+
SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
14913
|
+
SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
14914
|
+
ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
14915
|
+
VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
14916
|
+
CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
14917
|
+
INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
|
|
14918
|
+
CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
|
|
14919
|
+
AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
14920
|
+
SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
|
|
14921
|
+
requireSecretsSdk2 = createRequire2(import.meta.url);
|
|
14922
|
+
ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
14923
|
+
DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
14924
|
+
ClientTransportConfigurationError = class ClientTransportConfigurationError extends Error {
|
|
14925
|
+
appName;
|
|
14926
|
+
sources;
|
|
14927
|
+
constructor(appName, message, sources = []) {
|
|
14928
|
+
super(message);
|
|
14929
|
+
this.name = "ClientTransportConfigurationError";
|
|
14930
|
+
this.appName = appName;
|
|
14931
|
+
this.sources = Object.freeze([...sources]);
|
|
14932
|
+
}
|
|
14933
|
+
};
|
|
14934
|
+
HasnaHttpError = class HasnaHttpError extends Error {
|
|
14935
|
+
status;
|
|
14936
|
+
method;
|
|
14937
|
+
path;
|
|
14938
|
+
credentialSource;
|
|
14939
|
+
credentialTier;
|
|
14940
|
+
constructor(method, path, status, body, credential) {
|
|
14941
|
+
const guidance = credential ? `. ${credential.guidance}` : "";
|
|
14942
|
+
super(`Hasna cloud request failed: ${method} ${path} -> ${status}${guidance}`);
|
|
14943
|
+
this.name = "HasnaHttpError";
|
|
14944
|
+
this.status = status;
|
|
14945
|
+
this.method = method;
|
|
14946
|
+
this.path = path;
|
|
14947
|
+
Object.defineProperty(this, "body", {
|
|
14948
|
+
value: body,
|
|
14949
|
+
enumerable: status !== 401 && status !== 403,
|
|
14950
|
+
writable: false,
|
|
14951
|
+
configurable: false
|
|
14952
|
+
});
|
|
14953
|
+
this.credentialSource = credential?.source ?? null;
|
|
14954
|
+
this.credentialTier = credential?.tier ?? null;
|
|
14955
|
+
}
|
|
14956
|
+
};
|
|
14957
|
+
DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
14958
|
+
IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
14959
|
+
AUTHORITY_OVERRIDE_HEADERS2 = new Set([
|
|
14960
|
+
"host",
|
|
14961
|
+
":authority",
|
|
14962
|
+
"forwarded",
|
|
14963
|
+
"x-forwarded-host",
|
|
14964
|
+
"x-original-host"
|
|
14965
|
+
]);
|
|
14966
|
+
});
|
|
14967
|
+
|
|
14968
|
+
// src/lib/transport-resolver.ts
|
|
14969
|
+
function rethrowInstructionsAuthorityFailure(error) {
|
|
14970
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14971
|
+
const name = error instanceof Error ? error.name : "";
|
|
14972
|
+
const failure = (code, lead) => {
|
|
14973
|
+
throw new Error(`${code}: ${lead} ${message} There is no local fallback: local SQLite is opt-in only ` + `(${INSTRUCTIONS_LOCAL_OPT_IN_ENV}=1) and is disabled by default \u2014 failing closed`, { cause: error });
|
|
14974
|
+
};
|
|
14975
|
+
if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
|
|
14976
|
+
return failure("REMOTE_API_CREDENTIAL_INVALID", "The configured Instructions credential could not be used.");
|
|
14977
|
+
}
|
|
14978
|
+
if (/no API key could be resolved/.test(message)) {
|
|
14979
|
+
if (/is not set and no API key could be resolved/.test(message)) {
|
|
14980
|
+
return failure("REMOTE_API_CONFIG_MISSING", "no Instructions credential resolved from the Keychain item " + "hasna.credentials.instructions.api-key, ~/.hasna/instructions/config/credentials, " + `or ${clientTransportEnvKeys(INSTRUCTIONS_APP).apiKeyKeys[0]}.`);
|
|
14981
|
+
}
|
|
14982
|
+
return failure("REMOTE_API_KEY_MISSING", "an Instructions authority is configured but no API key resolved \u2014 looked in " + "hasna.credentials.instructions.api-key, ~/.hasna/instructions/config/credentials, " + `and ${clientTransportEnvKeys(INSTRUCTIONS_APP).apiKeyKeys[0]}.`);
|
|
14983
|
+
}
|
|
14984
|
+
return failure("REMOTE_API_URL_INVALID", "the configured Instructions authority is invalid.");
|
|
14985
|
+
}
|
|
14986
|
+
function resolveInstructionsStorageClient(env = process.env, options = {}) {
|
|
14987
|
+
const inputs = instructionsResolverInputs(env, options.credentials);
|
|
14988
|
+
try {
|
|
14989
|
+
return resolveStorageClient(INSTRUCTIONS_APP, inputs.env, {
|
|
14990
|
+
fetchImpl: (input, init) => fetch(input, { ...init, redirect: "manual" }),
|
|
14991
|
+
credentials: inputs.credentials
|
|
14992
|
+
});
|
|
14993
|
+
} catch (error) {
|
|
14994
|
+
rethrowInstructionsAuthorityFailure(error);
|
|
14995
|
+
}
|
|
14996
|
+
}
|
|
14997
|
+
function announceLocalInstructionsMode(write = (line) => process.stderr.write(`${line}
|
|
14998
|
+
`)) {
|
|
14999
|
+
if (localNoticePrinted)
|
|
15000
|
+
return false;
|
|
15001
|
+
localNoticePrinted = true;
|
|
15002
|
+
write(instructionsLocalModeNotice());
|
|
15003
|
+
return true;
|
|
15004
|
+
}
|
|
15005
|
+
var INSTRUCTIONS_APP = "instructions", INSTRUCTIONS_LOCAL_OPT_IN_ENV, localNoticePrinted = false;
|
|
15006
|
+
var init_transport_resolver = __esm(() => {
|
|
15007
|
+
init_transport();
|
|
15008
|
+
init_storage();
|
|
15009
|
+
init_local_opt_in();
|
|
15010
|
+
INSTRUCTIONS_LOCAL_OPT_IN_ENV = INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS[0];
|
|
15011
|
+
});
|
|
15012
|
+
|
|
13631
15013
|
// src/data/config-store.ts
|
|
13632
15014
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
13633
15015
|
function parseBoundedPagePayload(value, label) {
|
|
@@ -13635,7 +15017,7 @@ function parseBoundedPagePayload(value, label) {
|
|
|
13635
15017
|
const consumed = Number(page?.cursor) + (page?.items?.length ?? 0);
|
|
13636
15018
|
const complete = Boolean(page && Number.isSafeInteger(page.total) && consumed >= Number(page.total));
|
|
13637
15019
|
if (!page || !Array.isArray(page.items) || !Number.isSafeInteger(page.total) || Number(page.total) < 0 || !Number.isSafeInteger(page.limit) || Number(page.limit) < 1 || !Number.isSafeInteger(page.cursor) || Number(page.cursor) < 0 || page.items.length > Number(page.limit) || typeof page.has_more !== "boolean" || typeof page.complete !== "boolean" || page.truncated !== false || page.next_cursor !== null && !Number.isSafeInteger(page.next_cursor) || page.complete !== complete || page.has_more !== !complete || page.next_cursor !== (complete ? null : consumed)) {
|
|
13638
|
-
throw new
|
|
15020
|
+
throw new Error(`${label} returned an invalid or truncated bounded-read envelope`);
|
|
13639
15021
|
}
|
|
13640
15022
|
return {
|
|
13641
15023
|
...page,
|
|
@@ -13650,50 +15032,38 @@ function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
|
|
|
13650
15032
|
}
|
|
13651
15033
|
}
|
|
13652
15034
|
if (!Array.isArray(legacyItems)) {
|
|
13653
|
-
throw new
|
|
15035
|
+
throw new Error(`${label} returned neither a bounded envelope nor a complete legacy array`);
|
|
13654
15036
|
}
|
|
13655
15037
|
const normalized = normalizeBoundedReadOptions(options);
|
|
13656
15038
|
const page = boundedReadPage(legacyItems.slice(normalized.cursor, normalized.cursor + normalized.limit), legacyItems.length, normalized);
|
|
13657
15039
|
return { ...page, source_bounded: false };
|
|
13658
15040
|
}
|
|
13659
15041
|
function isCloudAuthError(err) {
|
|
13660
|
-
return err
|
|
15042
|
+
return typeof err === "object" && err !== null && err.name === "HasnaHttpError" && (err.status === 401 || err.status === 403);
|
|
15043
|
+
}
|
|
15044
|
+
function isNotFoundHttpError2(err) {
|
|
15045
|
+
return typeof err === "object" && err !== null && err.name === "HasnaHttpError" && err.status === 404;
|
|
13661
15046
|
}
|
|
13662
15047
|
function formatCliError(err, env = process.env) {
|
|
13663
15048
|
if (isCloudAuthError(err)) {
|
|
13664
|
-
const apiUrl = env[API_URL_ENV]?.trim();
|
|
13665
|
-
const detail = err.message?.trim();
|
|
13666
|
-
const serverNote = detail && !/^HTTP \d+\b/.test(detail) ? ` Server said: ${detail}` : "";
|
|
13667
15049
|
return [
|
|
13668
15050
|
`Instructions cloud API rejected the request (HTTP ${err.status}: authentication failed).`,
|
|
13669
|
-
|
|
13670
|
-
` The API key in ${API_KEY_ENV} is missing, expired, or revoked${apiUrl ? ` for ${apiUrl}` : ""}.`,
|
|
15051
|
+
` The API key in use is missing, expired, or revoked (the transport never echoes the server's 401/403 body).`,
|
|
13671
15052
|
` To continue, either:`,
|
|
13672
|
-
` - set a valid key: export
|
|
13673
|
-
`
|
|
13674
|
-
|
|
15053
|
+
` - set a valid key: export HASNA_INSTRUCTIONS_API_KEY=<new-key>`,
|
|
15054
|
+
` (or add the Keychain item hasna.credentials.instructions.api-key, or write`,
|
|
15055
|
+
` ~/.hasna/instructions/config/credentials)`,
|
|
15056
|
+
` - or opt in to the local store explicitly: export ${LOCAL_OPT_IN_ENV}=1`
|
|
15057
|
+
].join(`
|
|
13675
15058
|
`);
|
|
13676
15059
|
}
|
|
13677
15060
|
return err instanceof Error ? err.message : String(err);
|
|
13678
15061
|
}
|
|
13679
|
-
function resolveCloudConfig(env = process.env) {
|
|
13680
|
-
assertNoLegacyStorageMode(env);
|
|
13681
|
-
const apiUrl = env[API_URL_ENV]?.trim();
|
|
13682
|
-
const apiKey = env[API_KEY_ENV]?.trim();
|
|
13683
|
-
if (!apiUrl && !apiKey)
|
|
13684
|
-
return null;
|
|
13685
|
-
if (!apiUrl || !apiKey) {
|
|
13686
|
-
throw new Error(`API mode requires BOTH ${API_URL_ENV} and ${API_KEY_ENV}; only ` + `${apiUrl ? API_URL_ENV : API_KEY_ENV} is set. Set both to use the HTTP API, ` + `or unset both to use the local store.`);
|
|
13687
|
-
}
|
|
13688
|
-
return { apiUrl, apiKey };
|
|
13689
|
-
}
|
|
13690
|
-
function isApiTransport(env = process.env) {
|
|
13691
|
-
return resolveCloudConfig(env) !== null;
|
|
13692
|
-
}
|
|
13693
15062
|
|
|
13694
15063
|
class LocalConfigStore {
|
|
13695
15064
|
db;
|
|
13696
15065
|
mode = "local";
|
|
15066
|
+
v1BaseUrl = null;
|
|
13697
15067
|
constructor(db) {
|
|
13698
15068
|
this.db = db;
|
|
13699
15069
|
}
|
|
@@ -13822,50 +15192,22 @@ class LocalConfigStore {
|
|
|
13822
15192
|
|
|
13823
15193
|
class CloudConfigStore {
|
|
13824
15194
|
mode = "api";
|
|
13825
|
-
|
|
13826
|
-
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
this.
|
|
13830
|
-
this.apiKey = config.apiKey;
|
|
13831
|
-
this.timeoutMs = config.timeoutMs ?? 30000;
|
|
15195
|
+
v1BaseUrl;
|
|
15196
|
+
client;
|
|
15197
|
+
constructor(client) {
|
|
15198
|
+
this.client = client;
|
|
15199
|
+
this.v1BaseUrl = client.baseUrl;
|
|
13832
15200
|
}
|
|
13833
15201
|
async request(method, path, body, opts = {}) {
|
|
13834
|
-
const controller = new AbortController;
|
|
13835
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
13836
|
-
const headers = {
|
|
13837
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
13838
|
-
Accept: "application/json"
|
|
13839
|
-
};
|
|
13840
|
-
if (body !== undefined)
|
|
13841
|
-
headers["Content-Type"] = "application/json";
|
|
13842
|
-
if (opts.idempotent)
|
|
13843
|
-
headers["Idempotency-Key"] = randomUUID6();
|
|
13844
15202
|
try {
|
|
13845
|
-
const
|
|
13846
|
-
|
|
13847
|
-
headers,
|
|
13848
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
13849
|
-
signal: controller.signal
|
|
15203
|
+
const data = await this.client.transport.request(method, path, body, {
|
|
15204
|
+
...opts.idempotent ? { idempotencyKey: randomUUID6() } : {}
|
|
13850
15205
|
});
|
|
13851
|
-
|
|
15206
|
+
return { status: 200, data };
|
|
15207
|
+
} catch (err) {
|
|
15208
|
+
if (opts.allow404 && isNotFoundHttpError2(err))
|
|
13852
15209
|
return { status: 404, data: null };
|
|
13853
|
-
|
|
13854
|
-
let parsed = null;
|
|
13855
|
-
if (text) {
|
|
13856
|
-
try {
|
|
13857
|
-
parsed = JSON.parse(text);
|
|
13858
|
-
} catch {
|
|
13859
|
-
parsed = text;
|
|
13860
|
-
}
|
|
13861
|
-
}
|
|
13862
|
-
if (!res.ok) {
|
|
13863
|
-
const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
|
|
13864
|
-
throw new CloudHttpError(res.status, message, parsed);
|
|
13865
|
-
}
|
|
13866
|
-
return { status: res.status, data: parsed };
|
|
13867
|
-
} finally {
|
|
13868
|
-
clearTimeout(timer);
|
|
15210
|
+
throw err;
|
|
13869
15211
|
}
|
|
13870
15212
|
}
|
|
13871
15213
|
async listConfigs(filter = {}) {
|
|
@@ -14021,7 +15363,7 @@ class CloudConfigStore {
|
|
|
14021
15363
|
if (first.status !== 404) {
|
|
14022
15364
|
if (isUsable(first.data))
|
|
14023
15365
|
return first;
|
|
14024
|
-
throw new
|
|
15366
|
+
throw new Error(`Cloud /v1 returned an invalid profile response for ${pathForId(idOrSlug)}`);
|
|
14025
15367
|
}
|
|
14026
15368
|
const profiles = await this.listProfiles();
|
|
14027
15369
|
const profile = profiles.find((candidate) => candidate.id === idOrSlug || candidate.slug === idOrSlug);
|
|
@@ -14034,7 +15376,7 @@ class CloudConfigStore {
|
|
|
14034
15376
|
if (response.status !== 404) {
|
|
14035
15377
|
if (isUsable(response.data))
|
|
14036
15378
|
return response;
|
|
14037
|
-
throw new
|
|
15379
|
+
throw new Error(`Cloud /v1 returned an invalid profile response for ${pathForId(candidate)}`);
|
|
14038
15380
|
}
|
|
14039
15381
|
}
|
|
14040
15382
|
return { status: 404, data: null };
|
|
@@ -14117,7 +15459,7 @@ class CloudConfigStore {
|
|
|
14117
15459
|
}
|
|
14118
15460
|
if (data && "complete" in data) {
|
|
14119
15461
|
if (data.complete !== true || data.truncated !== false) {
|
|
14120
|
-
throw new
|
|
15462
|
+
throw new Error("Cloud /v1 profile resolve returned an incomplete or truncated read");
|
|
14121
15463
|
}
|
|
14122
15464
|
return { ...data, source_bounded: data.source_bounded ?? true };
|
|
14123
15465
|
}
|
|
@@ -14133,7 +15475,7 @@ class CloudConfigStore {
|
|
|
14133
15475
|
};
|
|
14134
15476
|
}
|
|
14135
15477
|
{
|
|
14136
|
-
throw new
|
|
15478
|
+
throw new Error("Cloud /v1 profile resolve returned an incomplete or truncated read");
|
|
14137
15479
|
}
|
|
14138
15480
|
}
|
|
14139
15481
|
async registerMachine(hostname2, os, arch2) {
|
|
@@ -14156,14 +15498,19 @@ class CloudConfigStore {
|
|
|
14156
15498
|
});
|
|
14157
15499
|
}
|
|
14158
15500
|
async reset() {
|
|
14159
|
-
throw new Error("`init --force` cannot wipe the shared cloud store from a client. " + "
|
|
15501
|
+
throw new Error("`init --force` cannot wipe the shared cloud store from a client. " + "Point this run at the local store (HASNA_INSTRUCTIONS_LOCAL=1 with no hosted credential) to reset it instead.");
|
|
14160
15502
|
}
|
|
14161
15503
|
}
|
|
14162
|
-
function resolveConfigStore(env = process.env) {
|
|
14163
|
-
|
|
14164
|
-
|
|
15504
|
+
function resolveConfigStore(env = process.env, options = {}) {
|
|
15505
|
+
if (selectsInstructionsLocalStore(env)) {
|
|
15506
|
+
if (env === process.env)
|
|
15507
|
+
announceLocalInstructionsMode();
|
|
15508
|
+
return new LocalConfigStore;
|
|
15509
|
+
}
|
|
15510
|
+
const { client } = resolveInstructionsStorageClient(env, options);
|
|
15511
|
+
return new CloudConfigStore(client);
|
|
14165
15512
|
}
|
|
14166
|
-
var
|
|
15513
|
+
var LOCAL_OPT_IN_ENV;
|
|
14167
15514
|
var init_config_store = __esm(() => {
|
|
14168
15515
|
init_configs();
|
|
14169
15516
|
init_profiles();
|
|
@@ -14173,22 +15520,14 @@ var init_config_store = __esm(() => {
|
|
|
14173
15520
|
init_types();
|
|
14174
15521
|
init_bounded_read();
|
|
14175
15522
|
init_instruction_graph();
|
|
14176
|
-
|
|
14177
|
-
|
|
14178
|
-
|
|
14179
|
-
body;
|
|
14180
|
-
constructor(status, message, body) {
|
|
14181
|
-
super(message);
|
|
14182
|
-
this.status = status;
|
|
14183
|
-
this.body = body;
|
|
14184
|
-
this.name = "CloudHttpError";
|
|
14185
|
-
}
|
|
14186
|
-
};
|
|
15523
|
+
init_transport_resolver();
|
|
15524
|
+
init_local_opt_in();
|
|
15525
|
+
LOCAL_OPT_IN_ENV = INSTRUCTIONS_LOCAL_OPT_IN_ENV;
|
|
14187
15526
|
});
|
|
14188
15527
|
|
|
14189
15528
|
// src/lib/session-render-ownership.ts
|
|
14190
|
-
import { existsSync as
|
|
14191
|
-
import { dirname as
|
|
15529
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
15530
|
+
import { dirname as dirname5, join as join13, parse as parse3, relative as relative3, sep } from "path";
|
|
14192
15531
|
function toSegments(absolutePath2) {
|
|
14193
15532
|
return absolutePath2.replaceAll("\\", "/").split("/").filter(Boolean);
|
|
14194
15533
|
}
|
|
@@ -14207,7 +15546,7 @@ function pathIsSessionRenderManagedDir(absolutePath2) {
|
|
|
14207
15546
|
function readManifestRelativePaths(manifestPath) {
|
|
14208
15547
|
let stats;
|
|
14209
15548
|
try {
|
|
14210
|
-
if (!
|
|
15549
|
+
if (!existsSync11(manifestPath))
|
|
14211
15550
|
return null;
|
|
14212
15551
|
stats = statSync4(manifestPath);
|
|
14213
15552
|
} catch {
|
|
@@ -14219,7 +15558,7 @@ function readManifestRelativePaths(manifestPath) {
|
|
|
14219
15558
|
}
|
|
14220
15559
|
let manifest;
|
|
14221
15560
|
try {
|
|
14222
|
-
manifest = JSON.parse(
|
|
15561
|
+
manifest = JSON.parse(readFileSync6(manifestPath, "utf-8"));
|
|
14223
15562
|
} catch {
|
|
14224
15563
|
return null;
|
|
14225
15564
|
}
|
|
@@ -14234,7 +15573,7 @@ function readManifestRelativePaths(manifestPath) {
|
|
|
14234
15573
|
}
|
|
14235
15574
|
function sessionRenderManifestClaimsPath(absolutePath2) {
|
|
14236
15575
|
const root = parse3(absolutePath2).root;
|
|
14237
|
-
let home =
|
|
15576
|
+
let home = dirname5(absolutePath2);
|
|
14238
15577
|
for (let depth = 0;depth < MANIFEST_ANCESTOR_LIMIT; depth += 1) {
|
|
14239
15578
|
const manifestPath = join13(home, ...SESSION_RENDER_MANIFEST_RELATIVE_PATH.split("/"));
|
|
14240
15579
|
const relativePaths = readManifestRelativePaths(manifestPath);
|
|
@@ -14243,7 +15582,7 @@ function sessionRenderManifestClaimsPath(absolutePath2) {
|
|
|
14243
15582
|
if (relativePaths.has(claimed))
|
|
14244
15583
|
return true;
|
|
14245
15584
|
}
|
|
14246
|
-
const parent =
|
|
15585
|
+
const parent = dirname5(home);
|
|
14247
15586
|
if (parent === home || home === root)
|
|
14248
15587
|
break;
|
|
14249
15588
|
home = parent;
|
|
@@ -14271,17 +15610,17 @@ __export(exports_apply, {
|
|
|
14271
15610
|
applyConfigs: () => applyConfigs,
|
|
14272
15611
|
applyConfig: () => applyConfig
|
|
14273
15612
|
});
|
|
14274
|
-
import { existsSync as
|
|
14275
|
-
import { basename as basename5, dirname as
|
|
14276
|
-
import { homedir as
|
|
15613
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync7, realpathSync as realpathSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
15614
|
+
import { basename as basename5, dirname as dirname6, join as join14, resolve as resolve10 } from "path";
|
|
15615
|
+
import { homedir as homedir8 } from "os";
|
|
14277
15616
|
function getConfigHome() {
|
|
14278
|
-
return process.env["CONFIGS_HOME"] || process.env["HOME"] ||
|
|
15617
|
+
return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir8();
|
|
14279
15618
|
}
|
|
14280
15619
|
function expandPath(p) {
|
|
14281
15620
|
if (p.startsWith("~/")) {
|
|
14282
|
-
return
|
|
15621
|
+
return resolve10(getConfigHome(), p.slice(2));
|
|
14283
15622
|
}
|
|
14284
|
-
return
|
|
15623
|
+
return resolve10(p);
|
|
14285
15624
|
}
|
|
14286
15625
|
function normalizeTargetPath(p) {
|
|
14287
15626
|
const expanded = expandPath(p);
|
|
@@ -14291,14 +15630,14 @@ function normalizeTargetPath(p) {
|
|
|
14291
15630
|
let current = expanded;
|
|
14292
15631
|
const missingSegments = [];
|
|
14293
15632
|
while (true) {
|
|
14294
|
-
if (
|
|
15633
|
+
if (existsSync12(current)) {
|
|
14295
15634
|
try {
|
|
14296
|
-
return
|
|
15635
|
+
return resolve10(realpathSync3(current), ...missingSegments);
|
|
14297
15636
|
} catch {
|
|
14298
15637
|
return expanded;
|
|
14299
15638
|
}
|
|
14300
15639
|
}
|
|
14301
|
-
const parent =
|
|
15640
|
+
const parent = dirname6(current);
|
|
14302
15641
|
const name = basename5(current);
|
|
14303
15642
|
if (parent === current)
|
|
14304
15643
|
return expanded;
|
|
@@ -14322,11 +15661,11 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
|
14322
15661
|
}
|
|
14323
15662
|
const path = expandPath(renderedTargetPath);
|
|
14324
15663
|
const renderedForTarget = isCursorGlobalAuthorityPath(path) ? stampCursorGlobalAuthorityMarker(renderedContent) : renderedContent;
|
|
14325
|
-
const previousContent =
|
|
15664
|
+
const previousContent = existsSync12(path) ? readFileSync7(path, "utf-8") : null;
|
|
14326
15665
|
const changed = previousContent !== renderedForTarget;
|
|
14327
15666
|
if (!opts.dryRun) {
|
|
14328
|
-
const dir =
|
|
14329
|
-
if (!
|
|
15667
|
+
const dir = dirname6(path);
|
|
15668
|
+
if (!existsSync12(dir)) {
|
|
14330
15669
|
mkdirSync3(dir, { recursive: true });
|
|
14331
15670
|
}
|
|
14332
15671
|
if (previousContent !== null && changed) {
|
|
@@ -14360,9 +15699,9 @@ function wouldDestroyACredential(targetPath, renderedContent, format) {
|
|
|
14360
15699
|
let current;
|
|
14361
15700
|
try {
|
|
14362
15701
|
const path = expandPath(targetPath);
|
|
14363
|
-
if (!
|
|
15702
|
+
if (!existsSync12(path))
|
|
14364
15703
|
return [];
|
|
14365
|
-
current =
|
|
15704
|
+
current = readFileSync7(path, "utf-8");
|
|
14366
15705
|
} catch {
|
|
14367
15706
|
return secretTokens;
|
|
14368
15707
|
}
|
|
@@ -14704,20 +16043,20 @@ var init_apply = __esm(() => {
|
|
|
14704
16043
|
});
|
|
14705
16044
|
|
|
14706
16045
|
// src/lib/sync-dir.ts
|
|
14707
|
-
import { existsSync as
|
|
16046
|
+
import { existsSync as existsSync13, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
|
|
14708
16047
|
import { join as join15, relative as relative4 } from "path";
|
|
14709
|
-
import { homedir as
|
|
16048
|
+
import { homedir as homedir9 } from "os";
|
|
14710
16049
|
function shouldSkip(p) {
|
|
14711
16050
|
return SKIP.some((s) => p.includes(s));
|
|
14712
16051
|
}
|
|
14713
16052
|
async function syncFromDir(dir, opts = {}) {
|
|
14714
16053
|
const store = opts.store ?? resolveConfigStore();
|
|
14715
16054
|
const absDir = expandPath(dir);
|
|
14716
|
-
if (!
|
|
16055
|
+
if (!existsSync13(absDir))
|
|
14717
16056
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
14718
|
-
const files = opts.recursive !== false ? walkDir(absDir) :
|
|
16057
|
+
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync2(absDir).map((f) => join15(absDir, f)).filter((f) => statSync5(f).isFile());
|
|
14719
16058
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
14720
|
-
const home =
|
|
16059
|
+
const home = homedir9();
|
|
14721
16060
|
const allConfigs = await store.listConfigs();
|
|
14722
16061
|
for (const file of files) {
|
|
14723
16062
|
if (shouldSkip(file)) {
|
|
@@ -14725,7 +16064,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
14725
16064
|
continue;
|
|
14726
16065
|
}
|
|
14727
16066
|
try {
|
|
14728
|
-
const content =
|
|
16067
|
+
const content = readFileSync8(file, "utf-8");
|
|
14729
16068
|
if (content.length > 500000) {
|
|
14730
16069
|
result.skipped.push(file + " (too large)");
|
|
14731
16070
|
continue;
|
|
@@ -14752,7 +16091,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
14752
16091
|
}
|
|
14753
16092
|
async function syncToDir(dir, opts = {}) {
|
|
14754
16093
|
const store = opts.store ?? resolveConfigStore();
|
|
14755
|
-
const home =
|
|
16094
|
+
const home = homedir9();
|
|
14756
16095
|
const absDir = expandPath(dir);
|
|
14757
16096
|
const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
|
|
14758
16097
|
const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
@@ -14775,7 +16114,7 @@ async function syncToDir(dir, opts = {}) {
|
|
|
14775
16114
|
return result;
|
|
14776
16115
|
}
|
|
14777
16116
|
function walkDir(dir, files = []) {
|
|
14778
|
-
for (const entry of
|
|
16117
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
14779
16118
|
const full = join15(dir, entry.name);
|
|
14780
16119
|
if (shouldSkip(full))
|
|
14781
16120
|
continue;
|
|
@@ -14811,7 +16150,7 @@ __export(exports_sync, {
|
|
|
14811
16150
|
KNOWN_CONFIGS: () => KNOWN_CONFIGS,
|
|
14812
16151
|
CLAUDE_PROMPT_OUTPUTS: () => CLAUDE_PROMPT_OUTPUTS
|
|
14813
16152
|
});
|
|
14814
|
-
import { existsSync as
|
|
16153
|
+
import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
|
|
14815
16154
|
import { basename as basename6, extname as extname3, join as join16 } from "path";
|
|
14816
16155
|
function claudeRuleOutputs(fileName) {
|
|
14817
16156
|
const stem = basename6(fileName, extname3(fileName));
|
|
@@ -14851,7 +16190,7 @@ function isGeneratedOutputTarget2(config, owners) {
|
|
|
14851
16190
|
return !!ownerIds && !ownerIds.has(config.id);
|
|
14852
16191
|
}
|
|
14853
16192
|
function hasClaudePromptSource() {
|
|
14854
|
-
return
|
|
16193
|
+
return existsSync14(expandPath("~/.claude/CLAUDE.md"));
|
|
14855
16194
|
}
|
|
14856
16195
|
function hasClaudeRuleSourceForCursorTarget(targetPath) {
|
|
14857
16196
|
const absoluteTargetPath = expandPath(targetPath);
|
|
@@ -14859,7 +16198,7 @@ function hasClaudeRuleSourceForCursorTarget(targetPath) {
|
|
|
14859
16198
|
if (!absoluteTargetPath.startsWith(`${absolutePrefix}/`) || !absoluteTargetPath.endsWith(".mdc"))
|
|
14860
16199
|
return false;
|
|
14861
16200
|
const stem = basename6(absoluteTargetPath, ".mdc");
|
|
14862
|
-
return
|
|
16201
|
+
return existsSync14(expandPath(`~/.claude/rules/${stem}.md`)) || existsSync14(expandPath(`~/.claude/rules/${stem}.mdc`));
|
|
14863
16202
|
}
|
|
14864
16203
|
function isKnownGeneratedTargetPath(targetPath) {
|
|
14865
16204
|
const normalizedTargetPath = normalizeTargetPath(targetPath);
|
|
@@ -14877,10 +16216,10 @@ async function syncProject(opts) {
|
|
|
14877
16216
|
const machine = detectMachineContext();
|
|
14878
16217
|
for (const pf of PROJECT_CONFIG_FILES) {
|
|
14879
16218
|
const abs = join16(absDir, pf.file);
|
|
14880
|
-
if (!
|
|
16219
|
+
if (!existsSync14(abs))
|
|
14881
16220
|
continue;
|
|
14882
16221
|
try {
|
|
14883
|
-
const rawContent =
|
|
16222
|
+
const rawContent = readFileSync9(abs, "utf-8");
|
|
14884
16223
|
if (rawContent.length > 500000) {
|
|
14885
16224
|
result.skipped.push(pf.file);
|
|
14886
16225
|
continue;
|
|
@@ -14917,12 +16256,12 @@ async function syncProject(opts) {
|
|
|
14917
16256
|
{ dir: join16(absDir, ".windsurf", "rules"), agent: "windsurf-legacy", namePrefix: "windsurf-rules" },
|
|
14918
16257
|
{ dir: join16(absDir, ".clinerules"), agent: "cline", namePrefix: "cline-rules" }
|
|
14919
16258
|
]) {
|
|
14920
|
-
if (!
|
|
16259
|
+
if (!existsSync14(ruleDir.dir))
|
|
14921
16260
|
continue;
|
|
14922
|
-
const mdFiles =
|
|
16261
|
+
const mdFiles = readdirSync3(ruleDir.dir).filter((f) => f.endsWith(".md") || f.endsWith(".mdc"));
|
|
14923
16262
|
for (const f of mdFiles) {
|
|
14924
16263
|
const abs = join16(ruleDir.dir, f);
|
|
14925
|
-
const raw =
|
|
16264
|
+
const raw = readFileSync9(abs, "utf-8");
|
|
14926
16265
|
const redacted = redactContent(raw, "markdown");
|
|
14927
16266
|
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
14928
16267
|
const content = machineAware.content;
|
|
@@ -14961,12 +16300,12 @@ async function syncKnown(opts = {}) {
|
|
|
14961
16300
|
for (const known of targets) {
|
|
14962
16301
|
if (known.rulesDir) {
|
|
14963
16302
|
const absDir = expandPath(known.rulesDir);
|
|
14964
|
-
if (!
|
|
16303
|
+
if (!existsSync14(absDir)) {
|
|
14965
16304
|
result.skipped.push(known.rulesDir);
|
|
14966
16305
|
continue;
|
|
14967
16306
|
}
|
|
14968
16307
|
const extensions = known.rulesExtensions ?? [".md", ".mdc"];
|
|
14969
|
-
const ruleFiles =
|
|
16308
|
+
const ruleFiles = readdirSync3(absDir).filter((f) => extensions.some((ext) => f.endsWith(ext)));
|
|
14970
16309
|
for (const f of ruleFiles) {
|
|
14971
16310
|
const abs2 = join16(absDir, f);
|
|
14972
16311
|
const targetPath = abs2.replace(home, "~");
|
|
@@ -14974,7 +16313,7 @@ async function syncKnown(opts = {}) {
|
|
|
14974
16313
|
result.skipped.push(`${targetPath} (generated output)`);
|
|
14975
16314
|
continue;
|
|
14976
16315
|
}
|
|
14977
|
-
const raw =
|
|
16316
|
+
const raw = readFileSync9(abs2, "utf-8");
|
|
14978
16317
|
const redacted = redactContent(raw, "markdown");
|
|
14979
16318
|
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
14980
16319
|
const content = machineAware.content;
|
|
@@ -15002,12 +16341,12 @@ async function syncKnown(opts = {}) {
|
|
|
15002
16341
|
continue;
|
|
15003
16342
|
}
|
|
15004
16343
|
const abs = expandPath(known.path);
|
|
15005
|
-
if (!
|
|
16344
|
+
if (!existsSync14(abs)) {
|
|
15006
16345
|
result.skipped.push(known.path);
|
|
15007
16346
|
continue;
|
|
15008
16347
|
}
|
|
15009
16348
|
try {
|
|
15010
|
-
const rawContent = normalizeKnownConfigSource(known,
|
|
16349
|
+
const rawContent = normalizeKnownConfigSource(known, readFileSync9(abs, "utf-8"));
|
|
15011
16350
|
if (rawContent.length > 500000) {
|
|
15012
16351
|
result.skipped.push(known.path + " (too large)");
|
|
15013
16352
|
continue;
|
|
@@ -15108,9 +16447,9 @@ function storedPlaceholderIsLiteralOnDisk(storedLine, diskLine) {
|
|
|
15108
16447
|
}
|
|
15109
16448
|
function buildDiff(expectedContent, targetPath, storedFormat, showSecrets) {
|
|
15110
16449
|
const path = expandPath(targetPath);
|
|
15111
|
-
if (!
|
|
16450
|
+
if (!existsSync14(path))
|
|
15112
16451
|
return `(file not found on disk: ${path})`;
|
|
15113
|
-
const diskContent =
|
|
16452
|
+
const diskContent = readFileSync9(path, "utf-8");
|
|
15114
16453
|
if (diskContent === expectedContent)
|
|
15115
16454
|
return "(no diff \u2014 identical)";
|
|
15116
16455
|
const format = redactFormatForTarget(targetPath, storedFormat);
|
|
@@ -15343,16 +16682,16 @@ __export(exports_package_manager_guard, {
|
|
|
15343
16682
|
scanPackageManagerSecrets: () => scanPackageManagerSecrets
|
|
15344
16683
|
});
|
|
15345
16684
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
15346
|
-
import { existsSync as
|
|
15347
|
-
import { homedir as
|
|
15348
|
-
import { basename as basename7, dirname as
|
|
16685
|
+
import { existsSync as existsSync23, lstatSync as lstatSync7, readdirSync as readdirSync6, readFileSync as readFileSync17 } from "fs";
|
|
16686
|
+
import { homedir as homedir12 } from "os";
|
|
16687
|
+
import { basename as basename7, dirname as dirname11, isAbsolute as isAbsolute6, join as join24, relative as relative7, resolve as resolve15 } from "path";
|
|
15349
16688
|
function scanPackageManagerSecrets(options = {}) {
|
|
15350
|
-
const cwd = options.cwd ?
|
|
15351
|
-
const roots = (options.roots && options.roots.length > 0 ? options.roots : [cwd]).map((root) =>
|
|
16689
|
+
const cwd = options.cwd ? resolve15(options.cwd) : process.cwd();
|
|
16690
|
+
const roots = (options.roots && options.roots.length > 0 ? options.roots : [cwd]).map((root) => resolve15(cwd, root));
|
|
15352
16691
|
const findings = [];
|
|
15353
16692
|
let scannedFiles = 0;
|
|
15354
16693
|
for (const root of roots) {
|
|
15355
|
-
if (!
|
|
16694
|
+
if (!existsSync23(root))
|
|
15356
16695
|
continue;
|
|
15357
16696
|
const stat = lstatSync7(root);
|
|
15358
16697
|
if (stat.isFile()) {
|
|
@@ -15362,7 +16701,7 @@ function scanPackageManagerSecrets(options = {}) {
|
|
|
15362
16701
|
if (text === null)
|
|
15363
16702
|
continue;
|
|
15364
16703
|
scannedFiles++;
|
|
15365
|
-
findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root),
|
|
16704
|
+
findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname11(root)));
|
|
15366
16705
|
continue;
|
|
15367
16706
|
}
|
|
15368
16707
|
if (!stat.isDirectory())
|
|
@@ -15379,10 +16718,10 @@ function scanPackageManagerSecrets(options = {}) {
|
|
|
15379
16718
|
}
|
|
15380
16719
|
}
|
|
15381
16720
|
if (options.includeHome) {
|
|
15382
|
-
const home =
|
|
16721
|
+
const home = homedir12();
|
|
15383
16722
|
for (const name of HOME_FILES) {
|
|
15384
16723
|
const file = join24(home, name);
|
|
15385
|
-
if (!
|
|
16724
|
+
if (!existsSync23(file))
|
|
15386
16725
|
continue;
|
|
15387
16726
|
const text = readTextFile(file);
|
|
15388
16727
|
if (text === null)
|
|
@@ -15402,7 +16741,7 @@ function scanPackageManagerSecrets(options = {}) {
|
|
|
15402
16741
|
function collectRepoFiles(root) {
|
|
15403
16742
|
const out = [];
|
|
15404
16743
|
const visit = (dir) => {
|
|
15405
|
-
for (const entry of
|
|
16744
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
15406
16745
|
if (entry.isDirectory()) {
|
|
15407
16746
|
if (SKIP_DIRS.has(entry.name))
|
|
15408
16747
|
continue;
|
|
@@ -15449,7 +16788,7 @@ function readTextFile(file) {
|
|
|
15449
16788
|
const stat = lstatSync7(file);
|
|
15450
16789
|
if (!stat.isFile() || stat.size > 5000000)
|
|
15451
16790
|
return null;
|
|
15452
|
-
const buf =
|
|
16791
|
+
const buf = readFileSync17(file);
|
|
15453
16792
|
if (buf.includes(0))
|
|
15454
16793
|
return null;
|
|
15455
16794
|
return buf.toString("utf-8");
|
|
@@ -15649,7 +16988,7 @@ function trackedFiles(root) {
|
|
|
15649
16988
|
}
|
|
15650
16989
|
function isTrackedFile(file) {
|
|
15651
16990
|
try {
|
|
15652
|
-
const repoRoot = execFileSync2("git", ["-C",
|
|
16991
|
+
const repoRoot = execFileSync2("git", ["-C", dirname11(file), "rev-parse", "--show-toplevel"], {
|
|
15653
16992
|
encoding: "utf-8",
|
|
15654
16993
|
stdio: ["ignore", "pipe", "ignore"]
|
|
15655
16994
|
}).trim();
|
|
@@ -15679,10 +17018,10 @@ function stripInlineComment(value) {
|
|
|
15679
17018
|
return value.replace(/\s[#;].*$/, "").trim();
|
|
15680
17019
|
}
|
|
15681
17020
|
function displayPath(file, root) {
|
|
15682
|
-
const home =
|
|
17021
|
+
const home = homedir12();
|
|
15683
17022
|
if (root === home && (file === home || file.startsWith(home + "/")))
|
|
15684
17023
|
return "~/" + toPosix(relative7(home, file));
|
|
15685
|
-
if (
|
|
17024
|
+
if (isAbsolute6(root) && file.startsWith(root + "/"))
|
|
15686
17025
|
return toPosix(relative7(root, file));
|
|
15687
17026
|
if (file === home || file.startsWith(home + "/"))
|
|
15688
17027
|
return "~/" + toPosix(relative7(home, file));
|
|
@@ -15734,12 +17073,12 @@ var init_package_manager_guard = __esm(() => {
|
|
|
15734
17073
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
15735
17074
|
import { Buffer as Buffer2 } from "buffer";
|
|
15736
17075
|
import { existsSync as existsSync2 } from "fs";
|
|
15737
|
-
import { join as
|
|
17076
|
+
import { join as join2 } from "path";
|
|
15738
17077
|
import { existsSync } from "fs";
|
|
15739
|
-
import { homedir as homedir2 } from "os";
|
|
15740
|
-
import { join as join2, resolve } from "path";
|
|
15741
17078
|
import { homedir } from "os";
|
|
15742
|
-
import { join } from "path";
|
|
17079
|
+
import { join, resolve } from "path";
|
|
17080
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
17081
|
+
import { join as pathsResolverJoin } from "path";
|
|
15743
17082
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
15744
17083
|
import { lookup as dnsLookup } from "dns/promises";
|
|
15745
17084
|
import { isIP } from "net";
|
|
@@ -15843,75 +17182,72 @@ function channelMatchesEvent(channel, event) {
|
|
|
15843
17182
|
return true;
|
|
15844
17183
|
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
15845
17184
|
}
|
|
15846
|
-
var
|
|
17185
|
+
var PATHS_RESOLVER_KIND_ENV = {
|
|
15847
17186
|
config: "HASNA_CONFIG_HOME",
|
|
15848
17187
|
data: "HASNA_DATA_HOME",
|
|
15849
17188
|
state: "HASNA_STATE_HOME",
|
|
15850
17189
|
cache: "HASNA_CACHE_HOME"
|
|
15851
17190
|
};
|
|
15852
|
-
var
|
|
15853
|
-
function
|
|
17191
|
+
var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
17192
|
+
function pathsResolverAssertApp(app) {
|
|
15854
17193
|
if (typeof app !== "string" || app.length === 0) {
|
|
15855
17194
|
throw new TypeError("paths: app must be a non-empty string");
|
|
15856
17195
|
}
|
|
15857
|
-
if (!
|
|
17196
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
15858
17197
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
15859
17198
|
}
|
|
15860
17199
|
}
|
|
15861
|
-
function
|
|
15862
|
-
|
|
15863
|
-
}
|
|
15864
|
-
|
|
15865
|
-
const value = envOf(options)[KIND_ENV[kind]];
|
|
15866
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
15867
|
-
}
|
|
15868
|
-
function isMacOS(platform) {
|
|
15869
|
-
return platform === "darwin";
|
|
17200
|
+
function pathsResolverAssertKind(kind) {
|
|
17201
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
17202
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
17203
|
+
}
|
|
15870
17204
|
}
|
|
15871
|
-
function
|
|
15872
|
-
|
|
15873
|
-
|
|
17205
|
+
function pathsResolverBaseDir(kind, options) {
|
|
17206
|
+
pathsResolverAssertKind(kind);
|
|
17207
|
+
const env = options.env ?? process.env;
|
|
17208
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
17209
|
+
if (typeof override === "string" && override.length > 0)
|
|
15874
17210
|
return override;
|
|
15875
|
-
const home = options.home ??
|
|
17211
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
15876
17212
|
const platform = options.platform ?? process.platform;
|
|
15877
|
-
if (
|
|
17213
|
+
if (platform === "darwin") {
|
|
15878
17214
|
switch (kind) {
|
|
15879
17215
|
case "config":
|
|
15880
17216
|
case "data":
|
|
15881
|
-
return
|
|
17217
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
15882
17218
|
case "cache":
|
|
15883
|
-
return
|
|
17219
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
15884
17220
|
case "state":
|
|
15885
|
-
return
|
|
17221
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
15886
17222
|
}
|
|
15887
17223
|
}
|
|
15888
17224
|
switch (kind) {
|
|
15889
17225
|
case "config":
|
|
15890
|
-
return
|
|
17226
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
15891
17227
|
case "data":
|
|
15892
|
-
return
|
|
17228
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
15893
17229
|
case "state":
|
|
15894
|
-
return
|
|
17230
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
15895
17231
|
case "cache":
|
|
15896
|
-
return
|
|
17232
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
15897
17233
|
}
|
|
15898
17234
|
}
|
|
15899
|
-
function
|
|
15900
|
-
|
|
15901
|
-
const appSegment = options.internal === true ?
|
|
15902
|
-
return
|
|
17235
|
+
function pathsResolverResolve(kind, options) {
|
|
17236
|
+
pathsResolverAssertApp(options.app);
|
|
17237
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
17238
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
15903
17239
|
}
|
|
15904
17240
|
function dataDir(options) {
|
|
15905
|
-
return
|
|
17241
|
+
return pathsResolverResolve("data", options);
|
|
15906
17242
|
}
|
|
15907
17243
|
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
15908
17244
|
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
15909
17245
|
var EVENTS_STORE_SENTINEL_FILE = "events.json";
|
|
15910
17246
|
function effectiveHome() {
|
|
15911
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
17247
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
15912
17248
|
}
|
|
15913
17249
|
function legacyHomeDir() {
|
|
15914
|
-
return
|
|
17250
|
+
return join(effectiveHome(), ".hasna", "events");
|
|
15915
17251
|
}
|
|
15916
17252
|
function resolverHome() {
|
|
15917
17253
|
return dataDir({ app: "events", home: effectiveHome() || undefined });
|
|
@@ -15920,7 +17256,7 @@ function adoptResolverHome(resolved, env = process.env) {
|
|
|
15920
17256
|
const dataOverride = env.HASNA_DATA_HOME;
|
|
15921
17257
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
15922
17258
|
return true;
|
|
15923
|
-
return existsSync(
|
|
17259
|
+
return existsSync(join(resolved, EVENTS_STORE_SENTINEL_FILE));
|
|
15924
17260
|
}
|
|
15925
17261
|
function exactEventsHome() {
|
|
15926
17262
|
const dir = process.env[HASNA_EVENTS_DIR_ENV];
|
|
@@ -15961,9 +17297,9 @@ class JsonEventsStore {
|
|
|
15961
17297
|
constructor(dataDir2 = getEventsDataDir()) {
|
|
15962
17298
|
this.dataDir = dataDir2;
|
|
15963
17299
|
this.runtime = localJsonRuntime(dataDir2);
|
|
15964
|
-
this.channelsPath =
|
|
15965
|
-
this.eventsPath =
|
|
15966
|
-
this.deliveriesPath =
|
|
17300
|
+
this.channelsPath = join2(dataDir2, "channels.json");
|
|
17301
|
+
this.eventsPath = join2(dataDir2, "events.json");
|
|
17302
|
+
this.deliveriesPath = join2(dataDir2, "deliveries.json");
|
|
15967
17303
|
}
|
|
15968
17304
|
async init() {
|
|
15969
17305
|
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
@@ -16231,7 +17567,7 @@ async function getEventsStatus(dataDir2) {
|
|
|
16231
17567
|
};
|
|
16232
17568
|
}
|
|
16233
17569
|
function statusFile(dataDir2, fileName, records) {
|
|
16234
|
-
const path =
|
|
17570
|
+
const path = join2(dataDir2, fileName);
|
|
16235
17571
|
return { path, exists: existsSync2(path), records };
|
|
16236
17572
|
}
|
|
16237
17573
|
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
@@ -17165,6 +18501,13 @@ function parseMatcherExpression(value, label) {
|
|
|
17165
18501
|
negated: false
|
|
17166
18502
|
};
|
|
17167
18503
|
}
|
|
18504
|
+
function webhookTargetPolicyFromEnv() {
|
|
18505
|
+
const value = process.env.HASNA_EVENTS_ALLOW_PRIVATE_WEBHOOK_TARGETS;
|
|
18506
|
+
if (!value)
|
|
18507
|
+
return;
|
|
18508
|
+
const hosts = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
18509
|
+
return hosts.length > 0 ? { allowPrivateHosts: hosts } : undefined;
|
|
18510
|
+
}
|
|
17168
18511
|
var DEFAULT_EVENT_LIST_LIMIT = 100;
|
|
17169
18512
|
function parseJsonObject(value, fallback) {
|
|
17170
18513
|
if (!value)
|
|
@@ -17190,7 +18533,7 @@ function parseHeaders(values) {
|
|
|
17190
18533
|
function createClient(options) {
|
|
17191
18534
|
if (options.createClient)
|
|
17192
18535
|
return options.createClient();
|
|
17193
|
-
return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
|
|
18536
|
+
return new EventsClient({ store: new JsonEventsStore(options.dataDir), webhookTargetPolicy: webhookTargetPolicyFromEnv() });
|
|
17194
18537
|
}
|
|
17195
18538
|
function print(value, json, text) {
|
|
17196
18539
|
if (json)
|
|
@@ -17271,6 +18614,8 @@ function registerChannelCommands(program, options) {
|
|
|
17271
18614
|
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
17272
18615
|
}, { honorFilters: actionOptions.honorFilters });
|
|
17273
18616
|
print(result, json, `${result.status}: ${result.channelId}`);
|
|
18617
|
+
if (result.status === "failed")
|
|
18618
|
+
process.exitCode = 1;
|
|
17274
18619
|
} catch (error) {
|
|
17275
18620
|
fail(error, json);
|
|
17276
18621
|
}
|
|
@@ -17379,9 +18724,9 @@ var {
|
|
|
17379
18724
|
// src/cli/index.tsx
|
|
17380
18725
|
init_apply();
|
|
17381
18726
|
import chalk from "chalk";
|
|
17382
|
-
import { existsSync as
|
|
17383
|
-
import { homedir as
|
|
17384
|
-
import { basename as basename8, join as join25, resolve as
|
|
18727
|
+
import { existsSync as existsSync24, lstatSync as lstatSync8, readFileSync as readFileSync18, readSync, writeSync } from "fs";
|
|
18728
|
+
import { homedir as homedir13 } from "os";
|
|
18729
|
+
import { basename as basename8, join as join25, resolve as resolve16 } from "path";
|
|
17385
18730
|
|
|
17386
18731
|
// src/lib/config-target-identity.ts
|
|
17387
18732
|
init_apply();
|
|
@@ -17427,13 +18772,13 @@ init_redact();
|
|
|
17427
18772
|
|
|
17428
18773
|
// src/lib/export.ts
|
|
17429
18774
|
init_config_store();
|
|
17430
|
-
import { existsSync as
|
|
17431
|
-
import { join as join17, resolve as
|
|
18775
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync4, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
18776
|
+
import { join as join17, resolve as resolve11 } from "path";
|
|
17432
18777
|
import { tmpdir } from "os";
|
|
17433
18778
|
async function exportConfigs(outputPath, opts = {}) {
|
|
17434
18779
|
const store = opts.store ?? resolveConfigStore();
|
|
17435
18780
|
const configs = await store.listConfigs(opts.filter);
|
|
17436
|
-
const absOutput =
|
|
18781
|
+
const absOutput = resolve11(outputPath);
|
|
17437
18782
|
const tmpDir = join17(tmpdir(), `configs-export-${Date.now()}`);
|
|
17438
18783
|
const contentsDir = join17(tmpDir, "contents");
|
|
17439
18784
|
try {
|
|
@@ -17459,7 +18804,7 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
17459
18804
|
}
|
|
17460
18805
|
return { path: absOutput, count: configs.length };
|
|
17461
18806
|
} finally {
|
|
17462
|
-
if (
|
|
18807
|
+
if (existsSync15(tmpDir)) {
|
|
17463
18808
|
rmSync3(tmpDir, { recursive: true, force: true });
|
|
17464
18809
|
}
|
|
17465
18810
|
}
|
|
@@ -17467,13 +18812,13 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
17467
18812
|
|
|
17468
18813
|
// src/lib/import.ts
|
|
17469
18814
|
init_config_store();
|
|
17470
|
-
import { existsSync as
|
|
17471
|
-
import { join as join18, resolve as
|
|
18815
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync10, rmSync as rmSync4 } from "fs";
|
|
18816
|
+
import { join as join18, resolve as resolve12 } from "path";
|
|
17472
18817
|
import { tmpdir as tmpdir2 } from "os";
|
|
17473
18818
|
async function importConfigs(bundlePath, opts = {}) {
|
|
17474
18819
|
const store = opts.store ?? resolveConfigStore();
|
|
17475
18820
|
const conflict = opts.conflict ?? "skip";
|
|
17476
|
-
const absPath =
|
|
18821
|
+
const absPath = resolve12(bundlePath);
|
|
17477
18822
|
const tmpDir = join18(tmpdir2(), `configs-import-${Date.now()}`);
|
|
17478
18823
|
const result = { created: 0, updated: 0, skipped: 0, errors: [] };
|
|
17479
18824
|
try {
|
|
@@ -17488,14 +18833,14 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
17488
18833
|
throw new Error(`tar extraction failed: ${stderr}`);
|
|
17489
18834
|
}
|
|
17490
18835
|
const manifestPath = join18(tmpDir, "manifest.json");
|
|
17491
|
-
if (!
|
|
18836
|
+
if (!existsSync16(manifestPath))
|
|
17492
18837
|
throw new Error("Invalid bundle: missing manifest.json");
|
|
17493
|
-
const manifest = JSON.parse(
|
|
18838
|
+
const manifest = JSON.parse(readFileSync10(manifestPath, "utf-8"));
|
|
17494
18839
|
for (const meta of manifest.configs) {
|
|
17495
18840
|
try {
|
|
17496
18841
|
const ext = meta.format === "text" ? "txt" : meta.format;
|
|
17497
18842
|
const contentFile = join18(tmpDir, "contents", `${meta.slug}.${ext}`);
|
|
17498
|
-
const content =
|
|
18843
|
+
const content = existsSync16(contentFile) ? readFileSync10(contentFile, "utf-8") : "";
|
|
17499
18844
|
let existing = null;
|
|
17500
18845
|
try {
|
|
17501
18846
|
existing = await store.getConfig(meta.slug);
|
|
@@ -17529,7 +18874,7 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
17529
18874
|
}
|
|
17530
18875
|
return result;
|
|
17531
18876
|
} finally {
|
|
17532
|
-
if (
|
|
18877
|
+
if (existsSync16(tmpDir)) {
|
|
17533
18878
|
rmSync4(tmpDir, { recursive: true, force: true });
|
|
17534
18879
|
}
|
|
17535
18880
|
}
|
|
@@ -17541,19 +18886,20 @@ init_machine();
|
|
|
17541
18886
|
|
|
17542
18887
|
// src/lib/session-apply.ts
|
|
17543
18888
|
init_project_context();
|
|
18889
|
+
init_session_render_state();
|
|
17544
18890
|
init_session_render();
|
|
17545
18891
|
init_cursor_authority();
|
|
17546
18892
|
init_session_authority();
|
|
17547
18893
|
import { createHash as createHash8, randomUUID as randomUUID7 } from "crypto";
|
|
17548
18894
|
import {
|
|
17549
|
-
existsSync as
|
|
18895
|
+
existsSync as existsSync17,
|
|
17550
18896
|
lstatSync as lstatSync4,
|
|
17551
18897
|
mkdirSync as mkdirSync6,
|
|
17552
|
-
readFileSync as
|
|
17553
|
-
readdirSync as
|
|
18898
|
+
readFileSync as readFileSync11,
|
|
18899
|
+
readdirSync as readdirSync4,
|
|
17554
18900
|
statSync as statSync6
|
|
17555
18901
|
} from "fs";
|
|
17556
|
-
import { dirname as
|
|
18902
|
+
import { dirname as dirname7, isAbsolute as isAbsolute5, join as join19, parse as parse4, relative as relative5, resolve as resolve13 } from "path";
|
|
17557
18903
|
|
|
17558
18904
|
class SessionApplyError extends Error {
|
|
17559
18905
|
constructor(message) {
|
|
@@ -17685,13 +19031,13 @@ function assertClaudeAuthorityStillClear(plan, targetHome, ownedClaudeAuthoritie
|
|
|
17685
19031
|
throw new SessionApplyError(`Claude authority changed after planning; refusing to apply: ${summary}`);
|
|
17686
19032
|
}
|
|
17687
19033
|
function ensureSessionTargetHome(targetHome) {
|
|
17688
|
-
if (!
|
|
19034
|
+
if (!existsSync17(targetHome))
|
|
17689
19035
|
mkdirSync6(targetHome, { recursive: true, mode: 448 });
|
|
17690
19036
|
assertSafeTargetHome(targetHome);
|
|
17691
19037
|
}
|
|
17692
19038
|
function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
17693
19039
|
const safeTargetHome = assertSafeTargetHome(targetHome);
|
|
17694
|
-
const resolvedManifestPath = manifestPath ? resolveManifestRelativePath(relative5(safeTargetHome,
|
|
19040
|
+
const resolvedManifestPath = manifestPath ? resolveManifestRelativePath(relative5(safeTargetHome, resolve13(manifestPath)), safeTargetHome) : resolve13(safeTargetHome, ".hasna", "session-render-manifest.json");
|
|
17695
19041
|
const checkedAt = new Date().toISOString();
|
|
17696
19042
|
const previousManifest = readPreviousManifest(resolvedManifestPath);
|
|
17697
19043
|
if (!previousManifest) {
|
|
@@ -17708,7 +19054,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
|
17708
19054
|
const drifted = [];
|
|
17709
19055
|
for (const file of previousManifest.files) {
|
|
17710
19056
|
const target = resolveManifestRelativePath(file.relativePath, safeTargetHome);
|
|
17711
|
-
if (!
|
|
19057
|
+
if (!existsSync17(target)) {
|
|
17712
19058
|
missing.push({
|
|
17713
19059
|
path: target,
|
|
17714
19060
|
relativePath: file.relativePath,
|
|
@@ -17718,7 +19064,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
|
17718
19064
|
});
|
|
17719
19065
|
continue;
|
|
17720
19066
|
}
|
|
17721
|
-
const actualSha256 = sha2568(
|
|
19067
|
+
const actualSha256 = sha2568(readFileSync11(target, "utf-8"));
|
|
17722
19068
|
if (actualSha256 !== file.sha256) {
|
|
17723
19069
|
drifted.push({
|
|
17724
19070
|
path: target,
|
|
@@ -17741,12 +19087,17 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
|
17741
19087
|
function restoreSessionRenderSnapshot(snapshotPath, options = {}) {
|
|
17742
19088
|
const snapshot = readSessionRenderSnapshot(snapshotPath);
|
|
17743
19089
|
const targetHome = assertSafeTargetHome(snapshot.targetHome);
|
|
17744
|
-
const resolvedSnapshotPath =
|
|
17745
|
-
const
|
|
17746
|
-
|
|
17747
|
-
|
|
19090
|
+
const resolvedSnapshotPath = resolve13(snapshotPath);
|
|
19091
|
+
const snapshotDir = getSessionRenderSnapshotDir(targetHome);
|
|
19092
|
+
const snapshotDirRelative = relative5(snapshotDir, resolvedSnapshotPath);
|
|
19093
|
+
const insideSnapshotDir = snapshotDirRelative !== ".." && !snapshotDirRelative.startsWith("../") && !isAbsolute5(snapshotDirRelative);
|
|
19094
|
+
if (!insideSnapshotDir) {
|
|
19095
|
+
const snapshotRelativePath = relative5(targetHome, resolvedSnapshotPath);
|
|
19096
|
+
if (snapshotRelativePath === "" || snapshotRelativePath === ".." || snapshotRelativePath.startsWith("../") || isAbsolute5(snapshotRelativePath)) {
|
|
19097
|
+
throw new SessionApplyError("Session snapshot must be stored inside its session-render snapshot location.");
|
|
19098
|
+
}
|
|
17748
19099
|
}
|
|
17749
|
-
assertNoSymlinkSegments2(targetHome, resolvedSnapshotPath);
|
|
19100
|
+
assertNoSymlinkSegments2(sessionRenderSnapshotWorkspaceRoot(targetHome), resolvedSnapshotPath);
|
|
17750
19101
|
const guard = observeProjectContextSessionGuard({
|
|
17751
19102
|
tool: snapshot.tool,
|
|
17752
19103
|
target_home: targetHome,
|
|
@@ -17859,8 +19210,8 @@ function requiredRestoreHash(file) {
|
|
|
17859
19210
|
return file.previousSha256;
|
|
17860
19211
|
}
|
|
17861
19212
|
function readSessionRenderSnapshot(snapshotPath) {
|
|
17862
|
-
const resolved =
|
|
17863
|
-
if (!
|
|
19213
|
+
const resolved = resolve13(snapshotPath);
|
|
19214
|
+
if (!existsSync17(resolved))
|
|
17864
19215
|
throw new SessionApplyError(`Session snapshot not found: ${snapshotPath}`);
|
|
17865
19216
|
const stat = lstatSync4(resolved);
|
|
17866
19217
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
@@ -17871,7 +19222,7 @@ function readSessionRenderSnapshot(snapshotPath) {
|
|
|
17871
19222
|
}
|
|
17872
19223
|
let parsed;
|
|
17873
19224
|
try {
|
|
17874
|
-
parsed = JSON.parse(
|
|
19225
|
+
parsed = JSON.parse(readFileSync11(resolved, "utf8"));
|
|
17875
19226
|
} catch {
|
|
17876
19227
|
throw new SessionApplyError(`Session snapshot is not valid JSON: ${snapshotPath}`);
|
|
17877
19228
|
}
|
|
@@ -17940,7 +19291,7 @@ function readSessionRenderSnapshot(snapshotPath) {
|
|
|
17940
19291
|
}
|
|
17941
19292
|
function reconstructPreRollbackLegacyV1Snapshot(snapshot, previousFiles, previousManifestFiles, targetHome, snapshotPath) {
|
|
17942
19293
|
assertNoNewerSessionSnapshot(snapshotPath, snapshot.createdAt, targetHome);
|
|
17943
|
-
const manifestPath =
|
|
19294
|
+
const manifestPath = resolve13(snapshot.manifestPath);
|
|
17944
19295
|
const manifestRelativePath = relative5(targetHome, manifestPath).replaceAll("\\", "/");
|
|
17945
19296
|
resolveSnapshotFilePath(manifestRelativePath, snapshot.manifestPath, targetHome);
|
|
17946
19297
|
const manifestSha256 = currentSessionFileHash(manifestPath, targetHome);
|
|
@@ -17949,7 +19300,7 @@ function reconstructPreRollbackLegacyV1Snapshot(snapshot, previousFiles, previou
|
|
|
17949
19300
|
}
|
|
17950
19301
|
let parsedManifest;
|
|
17951
19302
|
try {
|
|
17952
|
-
parsedManifest = JSON.parse(
|
|
19303
|
+
parsedManifest = JSON.parse(readFileSync11(manifestPath, "utf8"));
|
|
17953
19304
|
} catch {
|
|
17954
19305
|
throw new SessionApplyError(`Pre-rollback legacy v1 applied manifest is not valid JSON: ${snapshotPath}`);
|
|
17955
19306
|
}
|
|
@@ -17957,7 +19308,7 @@ function reconstructPreRollbackLegacyV1Snapshot(snapshot, previousFiles, previou
|
|
|
17957
19308
|
throw new SessionApplyError(`Pre-rollback legacy v1 applied manifest is invalid: ${snapshotPath}`);
|
|
17958
19309
|
}
|
|
17959
19310
|
const appliedManifest = parsedManifest;
|
|
17960
|
-
if (appliedManifest.schema !== SESSION_RENDER_SCHEMA || appliedManifest.tool !== snapshot.tool || appliedManifest.profile !== snapshot.profile || typeof appliedManifest.targetHome !== "string" ||
|
|
19311
|
+
if (appliedManifest.schema !== SESSION_RENDER_SCHEMA || appliedManifest.tool !== snapshot.tool || appliedManifest.profile !== snapshot.profile || typeof appliedManifest.targetHome !== "string" || resolve13(appliedManifest.targetHome) !== targetHome || appliedManifest.targetKind !== "session-home" && appliedManifest.targetKind !== "project-root" || !Array.isArray(appliedManifest.files)) {
|
|
17961
19312
|
throw new SessionApplyError(`Pre-rollback legacy v1 applied manifest does not match its snapshot: ${snapshotPath}`);
|
|
17962
19313
|
}
|
|
17963
19314
|
const afterFiles = [];
|
|
@@ -18041,17 +19392,17 @@ function assertNoNewerSessionSnapshot(snapshotPath, createdAt, targetHome) {
|
|
|
18041
19392
|
if (!Number.isFinite(createdAtMs)) {
|
|
18042
19393
|
throw new SessionApplyError(`Pre-rollback legacy v1 snapshot has an invalid creation time: ${snapshotPath}`);
|
|
18043
19394
|
}
|
|
18044
|
-
for (const entry of
|
|
18045
|
-
const candidatePath =
|
|
18046
|
-
if (candidatePath ===
|
|
19395
|
+
for (const entry of readdirSync4(dirname7(snapshotPath))) {
|
|
19396
|
+
const candidatePath = resolve13(dirname7(snapshotPath), entry);
|
|
19397
|
+
if (candidatePath === resolve13(snapshotPath) || !entry.endsWith(".json"))
|
|
18047
19398
|
continue;
|
|
18048
19399
|
const candidateStat = lstatSync4(candidatePath);
|
|
18049
19400
|
if (candidateStat.isSymbolicLink() || !candidateStat.isFile() || candidateStat.size > 32 * 1024 * 1024)
|
|
18050
19401
|
continue;
|
|
18051
19402
|
try {
|
|
18052
|
-
const candidate = JSON.parse(
|
|
19403
|
+
const candidate = JSON.parse(readFileSync11(candidatePath, "utf8"));
|
|
18053
19404
|
const candidateCreatedAtMs = typeof candidate.createdAt === "string" ? Date.parse(candidate.createdAt) : Number.NaN;
|
|
18054
|
-
if ((candidate.schema === "hasna.configs.session-render-snapshot/v1" || candidate.schema === "hasna.configs.session-render-snapshot/v2") && typeof candidate.targetHome === "string" &&
|
|
19405
|
+
if ((candidate.schema === "hasna.configs.session-render-snapshot/v1" || candidate.schema === "hasna.configs.session-render-snapshot/v2") && typeof candidate.targetHome === "string" && resolve13(candidate.targetHome) === targetHome && Number.isFinite(candidateCreatedAtMs) && candidateCreatedAtMs >= createdAtMs) {
|
|
18055
19406
|
throw new SessionApplyError(`Cannot restore pre-rollback legacy v1 snapshot after a newer session snapshot exists: ${candidatePath}`);
|
|
18056
19407
|
}
|
|
18057
19408
|
} catch (error) {
|
|
@@ -18120,14 +19471,14 @@ function inferLegacySnapshotAction(file, previousFiles, previousManifestFiles, p
|
|
|
18120
19471
|
}
|
|
18121
19472
|
function resolveSnapshotFilePath(relativePath, recordedPath, targetHome) {
|
|
18122
19473
|
const path = resolveManifestRelativePath(relativePath, targetHome);
|
|
18123
|
-
if (
|
|
19474
|
+
if (resolve13(recordedPath) !== path) {
|
|
18124
19475
|
throw new SessionApplyError(`Session snapshot file path mismatch for ${relativePath}`);
|
|
18125
19476
|
}
|
|
18126
19477
|
return path;
|
|
18127
19478
|
}
|
|
18128
19479
|
function planFileResult(plan, file, targetHome, previousHashes, previousManifest, options) {
|
|
18129
19480
|
const target = resolvePlannedFilePath(plan, file, targetHome);
|
|
18130
|
-
const previousContent =
|
|
19481
|
+
const previousContent = existsSync17(target) ? readFileSync11(target, "utf-8") : null;
|
|
18131
19482
|
const previousSha256 = previousContent === null ? null : sha2568(previousContent);
|
|
18132
19483
|
const previouslyManaged = isPreviouslyManaged(file, previousSha256, previousHashes, previousManifest);
|
|
18133
19484
|
const changed = previousContent !== file.content;
|
|
@@ -18226,9 +19577,9 @@ function planStaleFileResults(plan, targetHome, previousManifest, currentRelativ
|
|
|
18226
19577
|
}
|
|
18227
19578
|
function planStaleFileResult(file, targetHome, options) {
|
|
18228
19579
|
const target = resolveManifestRelativePath(file.relativePath, targetHome);
|
|
18229
|
-
if (!
|
|
19580
|
+
if (!existsSync17(target))
|
|
18230
19581
|
return null;
|
|
18231
|
-
const previousContent =
|
|
19582
|
+
const previousContent = readFileSync11(target, "utf-8");
|
|
18232
19583
|
const previousSha256 = sha2568(previousContent);
|
|
18233
19584
|
if (!options.force && previousSha256 !== file.sha256) {
|
|
18234
19585
|
return {
|
|
@@ -18273,31 +19624,31 @@ function isPreviouslyManaged(file, previousSha256, previousHashes, previousManif
|
|
|
18273
19624
|
return previousHashes.get(file.relativePath) === previousSha256;
|
|
18274
19625
|
}
|
|
18275
19626
|
function resolvePlannedFilePath(plan, file, targetHome) {
|
|
18276
|
-
const target =
|
|
19627
|
+
const target = resolve13(targetHome, ...file.relativePath.split("/"));
|
|
18277
19628
|
const rel = relative5(targetHome, target);
|
|
18278
|
-
if (rel === "" || rel === ".." || rel.startsWith("../") ||
|
|
19629
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute5(rel)) {
|
|
18279
19630
|
throw new SessionApplyError(`Session file escapes target home: ${file.relativePath}`);
|
|
18280
19631
|
}
|
|
18281
|
-
if (
|
|
19632
|
+
if (resolve13(file.path) !== target) {
|
|
18282
19633
|
throw new SessionApplyError(`Session file path mismatch for ${file.relativePath}: ${file.path}`);
|
|
18283
19634
|
}
|
|
18284
19635
|
assertNoSymlinkSegments2(targetHome, target);
|
|
18285
19636
|
return target;
|
|
18286
19637
|
}
|
|
18287
19638
|
function resolveManifestRelativePath(relativePath, targetHome) {
|
|
18288
|
-
const target =
|
|
19639
|
+
const target = resolve13(targetHome, ...relativePath.split(/[\\/]+/));
|
|
18289
19640
|
const rel = relative5(targetHome, target);
|
|
18290
|
-
if (rel === "" || rel === ".." || rel.startsWith("../") ||
|
|
19641
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute5(rel)) {
|
|
18291
19642
|
throw new SessionApplyError(`Session manifest file escapes target home: ${relativePath}`);
|
|
18292
19643
|
}
|
|
18293
19644
|
assertNoSymlinkSegments2(targetHome, target);
|
|
18294
19645
|
return target;
|
|
18295
19646
|
}
|
|
18296
19647
|
function readPreviousManifest(path) {
|
|
18297
|
-
if (!
|
|
19648
|
+
if (!existsSync17(path))
|
|
18298
19649
|
return null;
|
|
18299
19650
|
try {
|
|
18300
|
-
const parsed = JSON.parse(
|
|
19651
|
+
const parsed = JSON.parse(readFileSync11(path, "utf-8"));
|
|
18301
19652
|
if (parsed.schema !== SESSION_RENDER_SCHEMA)
|
|
18302
19653
|
return null;
|
|
18303
19654
|
if (!Array.isArray(parsed.files))
|
|
@@ -18336,13 +19687,13 @@ function assertExpectedSessionFileHash(path, targetHome, expectedHash) {
|
|
|
18336
19687
|
}
|
|
18337
19688
|
function currentSessionFileHash(path, targetHome) {
|
|
18338
19689
|
assertNoSymlinkSegments2(targetHome, path);
|
|
18339
|
-
if (!
|
|
19690
|
+
if (!existsSync17(path))
|
|
18340
19691
|
return null;
|
|
18341
19692
|
const stat = lstatSync4(path);
|
|
18342
19693
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
18343
19694
|
throw new SessionApplyError(`Session apply path is not a regular file: ${path}`);
|
|
18344
19695
|
}
|
|
18345
|
-
return sha2568(
|
|
19696
|
+
return sha2568(readFileSync11(path, "utf-8"));
|
|
18346
19697
|
}
|
|
18347
19698
|
function requiredPreviousHash(result) {
|
|
18348
19699
|
if (result.previousSha256 === null) {
|
|
@@ -18351,8 +19702,8 @@ function requiredPreviousHash(result) {
|
|
|
18351
19702
|
return result.previousSha256;
|
|
18352
19703
|
}
|
|
18353
19704
|
function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps) {
|
|
18354
|
-
const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").filter((result) =>
|
|
18355
|
-
const content =
|
|
19705
|
+
const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").filter((result) => existsSync17(result.path)).map((result) => {
|
|
19706
|
+
const content = readFileSync11(result.path, "utf-8");
|
|
18356
19707
|
return {
|
|
18357
19708
|
path: result.path,
|
|
18358
19709
|
relativePath: result.relativePath,
|
|
@@ -18370,7 +19721,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
18370
19721
|
};
|
|
18371
19722
|
}
|
|
18372
19723
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
18373
|
-
const snapshotPath =
|
|
19724
|
+
const snapshotPath = join19(getSessionRenderSnapshotDir(targetHome), `${timestamp}-${randomUUID7()}.json`);
|
|
18374
19725
|
const afterFiles = results.map((result) => {
|
|
18375
19726
|
if (result.action === "conflict") {
|
|
18376
19727
|
throw new SessionApplyError(`Cannot snapshot unresolved conflict: ${result.relativePath}`);
|
|
@@ -18400,7 +19751,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
18400
19751
|
path: snapshotPath,
|
|
18401
19752
|
content: `${JSON.stringify(snapshot, null, 2)}
|
|
18402
19753
|
`,
|
|
18403
|
-
workspace_root: targetHome,
|
|
19754
|
+
workspace_root: sessionRenderSnapshotWorkspaceRoot(targetHome),
|
|
18404
19755
|
default_mode: 384,
|
|
18405
19756
|
expected_hash: null,
|
|
18406
19757
|
max_observed_bytes: null,
|
|
@@ -18416,14 +19767,14 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
18416
19767
|
};
|
|
18417
19768
|
}
|
|
18418
19769
|
function assertSafeTargetHome(targetHome) {
|
|
18419
|
-
if (!
|
|
19770
|
+
if (!isAbsolute5(targetHome))
|
|
18420
19771
|
throw new SessionApplyError(`Session target home must be absolute: ${targetHome}`);
|
|
18421
|
-
const normalized =
|
|
19772
|
+
const normalized = resolve13(targetHome);
|
|
18422
19773
|
if (normalized === parse4(normalized).root) {
|
|
18423
19774
|
throw new SessionApplyError(`Session target home cannot be the filesystem root: ${targetHome}`);
|
|
18424
19775
|
}
|
|
18425
19776
|
assertNoSymlinkAncestors2(normalized);
|
|
18426
|
-
if (
|
|
19777
|
+
if (existsSync17(normalized) && lstatSync4(normalized).isSymbolicLink()) {
|
|
18427
19778
|
throw new SessionApplyError(`Session target home cannot be a symlink: ${normalized}`);
|
|
18428
19779
|
}
|
|
18429
19780
|
return normalized;
|
|
@@ -18434,19 +19785,19 @@ function assertNoSymlinkSegments2(root, target) {
|
|
|
18434
19785
|
let current = root;
|
|
18435
19786
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
18436
19787
|
current = join19(current, segment);
|
|
18437
|
-
if (
|
|
19788
|
+
if (existsSync17(current) && lstatSync4(current).isSymbolicLink()) {
|
|
18438
19789
|
throw new SessionApplyError(`Session apply path uses a symlink: ${current}`);
|
|
18439
19790
|
}
|
|
18440
19791
|
}
|
|
18441
19792
|
}
|
|
18442
19793
|
function assertNoSymlinkAncestors2(path) {
|
|
18443
|
-
const normalized =
|
|
19794
|
+
const normalized = resolve13(path);
|
|
18444
19795
|
const parsed = parse4(normalized);
|
|
18445
19796
|
let current = parsed.root;
|
|
18446
19797
|
const rel = relative5(parsed.root, normalized);
|
|
18447
19798
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
18448
19799
|
current = join19(current, segment);
|
|
18449
|
-
if (!
|
|
19800
|
+
if (!existsSync17(current))
|
|
18450
19801
|
return;
|
|
18451
19802
|
if (lstatSync4(current).isSymbolicLink()) {
|
|
18452
19803
|
throw new SessionApplyError(`Session apply path uses a symlink ancestor: ${current}`);
|
|
@@ -18496,10 +19847,10 @@ function formatGlobalSourceCoverageWarnings(result) {
|
|
|
18496
19847
|
|
|
18497
19848
|
// src/lib/station-profile.ts
|
|
18498
19849
|
init_raw_store_root();
|
|
18499
|
-
import { spawnSync } from "child_process";
|
|
18500
|
-
import { existsSync as
|
|
18501
|
-
import { arch as osArch, homedir as
|
|
18502
|
-
import { dirname as
|
|
19850
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
19851
|
+
import { existsSync as existsSync18, lstatSync as lstatSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync12, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
19852
|
+
import { arch as osArch, homedir as homedir10, hostname as osHostname2, platform as osPlatform, userInfo as osUserInfo } from "os";
|
|
19853
|
+
import { dirname as dirname8, join as join20 } from "path";
|
|
18503
19854
|
var STATION_PROFILE_CACHE_FILENAME = "station-profile.md";
|
|
18504
19855
|
var STATION_PROFILE_SOURCE_ID = "station-profile";
|
|
18505
19856
|
var STATION_PROFILE_LAYER = "machine";
|
|
@@ -18509,23 +19860,23 @@ var STATION_PROFILE_FULL_NAMES_MAX = 6;
|
|
|
18509
19860
|
var STATION_PROFILE_PRIMARY_SCOPE = "@hasna";
|
|
18510
19861
|
var MACHINES_MANIFEST_PATH_ENV = "HASNA_MACHINES_MANIFEST_PATH";
|
|
18511
19862
|
var BUN_INSTALL_ENV = "BUN_INSTALL";
|
|
18512
|
-
function
|
|
18513
|
-
return env["HOME"] || env["USERPROFILE"] ||
|
|
19863
|
+
function homeDir5(env = process.env) {
|
|
19864
|
+
return env["HOME"] || env["USERPROFILE"] || homedir10();
|
|
18514
19865
|
}
|
|
18515
19866
|
function getStationProfileCachePath(env = process.env) {
|
|
18516
19867
|
return join20(getRawStoreRoot(env), STATION_PROFILE_CACHE_FILENAME);
|
|
18517
19868
|
}
|
|
18518
19869
|
function getMachinesManifestPath(env = process.env) {
|
|
18519
|
-
return env[MACHINES_MANIFEST_PATH_ENV] || join20(
|
|
19870
|
+
return env[MACHINES_MANIFEST_PATH_ENV] || join20(homeDir5(env), ".hasna", "machines", "machines.json");
|
|
18520
19871
|
}
|
|
18521
19872
|
function getBunGlobalModulesDir(env = process.env) {
|
|
18522
|
-
return join20(env[BUN_INSTALL_ENV] || join20(
|
|
19873
|
+
return join20(env[BUN_INSTALL_ENV] || join20(homeDir5(env), ".bun"), "install", "global", "node_modules");
|
|
18523
19874
|
}
|
|
18524
19875
|
function readMachinesManifest(path) {
|
|
18525
19876
|
try {
|
|
18526
|
-
if (!
|
|
19877
|
+
if (!existsSync18(path))
|
|
18527
19878
|
return null;
|
|
18528
|
-
const parsed = JSON.parse(
|
|
19879
|
+
const parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
18529
19880
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
18530
19881
|
return null;
|
|
18531
19882
|
const machines = parsed["machines"];
|
|
@@ -18555,7 +19906,7 @@ function metadataUser(record) {
|
|
|
18555
19906
|
}
|
|
18556
19907
|
function probeMachineStatus(machineId) {
|
|
18557
19908
|
try {
|
|
18558
|
-
const result =
|
|
19909
|
+
const result = spawnSync2("machines", ["details", "--json", "--machine", machineId], {
|
|
18559
19910
|
encoding: "utf8",
|
|
18560
19911
|
timeout: 3000,
|
|
18561
19912
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -18577,9 +19928,9 @@ function probeMachineStatus(machineId) {
|
|
|
18577
19928
|
}
|
|
18578
19929
|
}
|
|
18579
19930
|
function resolveStationProfileMachine(env = process.env, options = {}) {
|
|
18580
|
-
const hostname2 =
|
|
19931
|
+
const hostname2 = osHostname2();
|
|
18581
19932
|
const record = findLocalManifestMachine(readMachinesManifest(getMachinesManifestPath(env)), hostname2);
|
|
18582
|
-
const home =
|
|
19933
|
+
const home = homeDir5(env);
|
|
18583
19934
|
const platform = stringField(record, "platform") ?? osPlatform();
|
|
18584
19935
|
const workspacePath = stringField(record, "workspacePath") ?? join20(home, platform === "darwin" ? "Workspace" : "workspace");
|
|
18585
19936
|
const machine = {
|
|
@@ -18600,7 +19951,7 @@ function resolveStationProfileMachine(env = process.env, options = {}) {
|
|
|
18600
19951
|
function scopedPackageNames(modulesDir, scope) {
|
|
18601
19952
|
const scopeDir = join20(modulesDir, scope);
|
|
18602
19953
|
try {
|
|
18603
|
-
if (!
|
|
19954
|
+
if (!existsSync18(scopeDir))
|
|
18604
19955
|
return null;
|
|
18605
19956
|
return readdirNames(scopeDir).sort();
|
|
18606
19957
|
} catch {
|
|
@@ -18608,7 +19959,7 @@ function scopedPackageNames(modulesDir, scope) {
|
|
|
18608
19959
|
}
|
|
18609
19960
|
}
|
|
18610
19961
|
function readdirNames(dir) {
|
|
18611
|
-
return
|
|
19962
|
+
return readdirSync5(dir).filter((name) => {
|
|
18612
19963
|
try {
|
|
18613
19964
|
return lstatSync5(join20(dir, name)).isDirectory();
|
|
18614
19965
|
} catch {
|
|
@@ -18620,7 +19971,7 @@ function resolveStationProfilePackages(env = process.env) {
|
|
|
18620
19971
|
const modulesDir = getBunGlobalModulesDir(env);
|
|
18621
19972
|
let scopeDirs;
|
|
18622
19973
|
try {
|
|
18623
|
-
if (!
|
|
19974
|
+
if (!existsSync18(modulesDir))
|
|
18624
19975
|
return null;
|
|
18625
19976
|
scopeDirs = readdirNames(modulesDir).filter((name) => name.startsWith("@") && name.toLowerCase().includes("hasna"));
|
|
18626
19977
|
} catch {
|
|
@@ -18695,9 +20046,9 @@ function refreshStationProfile(options = {}) {
|
|
|
18695
20046
|
const path = getStationProfileCachePath(env);
|
|
18696
20047
|
const generatedAt = new Date().toISOString();
|
|
18697
20048
|
if (!options.dryRun) {
|
|
18698
|
-
const existing =
|
|
20049
|
+
const existing = existsSync18(path) ? readFileSync12(path, "utf8") : null;
|
|
18699
20050
|
if (existing !== content) {
|
|
18700
|
-
mkdirSync7(
|
|
20051
|
+
mkdirSync7(dirname8(path), { recursive: true });
|
|
18701
20052
|
writeFileSync4(path, content, "utf8");
|
|
18702
20053
|
}
|
|
18703
20054
|
}
|
|
@@ -18714,9 +20065,9 @@ function refreshStationProfile(options = {}) {
|
|
|
18714
20065
|
function readStationProfile(env = process.env) {
|
|
18715
20066
|
const path = getStationProfileCachePath(env);
|
|
18716
20067
|
try {
|
|
18717
|
-
if (!
|
|
20068
|
+
if (!existsSync18(path))
|
|
18718
20069
|
return null;
|
|
18719
|
-
return
|
|
20070
|
+
return readFileSync12(path, "utf8");
|
|
18720
20071
|
} catch {
|
|
18721
20072
|
return null;
|
|
18722
20073
|
}
|
|
@@ -18743,9 +20094,6 @@ init_config_store();
|
|
|
18743
20094
|
init_config_store();
|
|
18744
20095
|
var PROJECT_DASHBOARD_STANDARD_SLUG = "agent-managed-project-dashboard-standard";
|
|
18745
20096
|
var PROJECT_DASHBOARD_PROFILE_VARIABLES = {
|
|
18746
|
-
PROJECT_DASHBOARD_DIR: ".hasna/project",
|
|
18747
|
-
PROJECT_DASHBOARD_RENDER_MANIFEST: ".hasna/project/dashboard/render.json",
|
|
18748
|
-
PROJECT_DASHBOARD_SNAPSHOTS_DIR: ".hasna/project/dashboard/snapshots",
|
|
18749
20097
|
PROJECT_CHANNEL_PREFIX: ""
|
|
18750
20098
|
};
|
|
18751
20099
|
var PROJECT_DASHBOARD_STANDARD_CONTENT = `# Agent-Managed Project Dashboard Standard
|
|
@@ -18756,9 +20104,13 @@ evidence, tasks, knowledge, and dashboard output consistent.
|
|
|
18756
20104
|
|
|
18757
20105
|
## Canonical Files
|
|
18758
20106
|
|
|
18759
|
-
|
|
18760
|
-
|
|
18761
|
-
|
|
20107
|
+
There is exactly one project-layout convention: the canonical per-workspace
|
|
20108
|
+
store \`~/.hasna/projects/workspaces/<workspace_id>/\`. Never create a project
|
|
20109
|
+
layout directory inside the project folder itself.
|
|
20110
|
+
|
|
20111
|
+
- Per-workspace store root: \`~/.hasna/projects/workspaces/<workspace_id>/\`
|
|
20112
|
+
- Dashboard render manifest: \`~/.hasna/projects/workspaces/<workspace_id>/dashboard/render.json\`
|
|
20113
|
+
- Latest snapshot: \`~/.hasna/projects/workspaces/<workspace_id>/dashboard/snapshots/latest.snapshot.json\`
|
|
18762
20114
|
- Dashboard schema ids come from \`@hasna/contracts\`.
|
|
18763
20115
|
- Project folders may contain private documents, but render JSON must contain
|
|
18764
20116
|
only ids, counts, statuses, resource refs, evidence refs, and redacted
|
|
@@ -19051,18 +20403,18 @@ init_codewith_shared_todos_storage_standard();
|
|
|
19051
20403
|
|
|
19052
20404
|
// src/lib/managed-skill-runtimes.ts
|
|
19053
20405
|
import { createHash as createHash9 } from "crypto";
|
|
19054
|
-
import { spawnSync as
|
|
20406
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
19055
20407
|
import {
|
|
19056
|
-
existsSync as
|
|
20408
|
+
existsSync as existsSync19,
|
|
19057
20409
|
lstatSync as lstatSync6,
|
|
19058
20410
|
mkdirSync as mkdirSync8,
|
|
19059
|
-
readFileSync as
|
|
20411
|
+
readFileSync as readFileSync13,
|
|
19060
20412
|
renameSync as renameSync2,
|
|
19061
20413
|
rmSync as rmSync5,
|
|
19062
20414
|
writeFileSync as writeFileSync5
|
|
19063
20415
|
} from "fs";
|
|
19064
|
-
import { homedir as
|
|
19065
|
-
import { dirname as
|
|
20416
|
+
import { homedir as homedir11 } from "os";
|
|
20417
|
+
import { dirname as dirname9, join as join21, parse as parse5, relative as relative6, resolve as resolve14 } from "path";
|
|
19066
20418
|
var INBOX_CONVERSATIONS_MINIMUM_VERSION = "0.5.28";
|
|
19067
20419
|
var INBOX_SKILL_MARKERS = [
|
|
19068
20420
|
[".claude", "skills", "inbox", "SKILL.md"],
|
|
@@ -19083,13 +20435,13 @@ function lstatOrNull(path) {
|
|
|
19083
20435
|
}
|
|
19084
20436
|
}
|
|
19085
20437
|
function findSymlinkedAncestor(path) {
|
|
19086
|
-
const normalized =
|
|
20438
|
+
const normalized = resolve14(path);
|
|
19087
20439
|
const parsed = parse5(normalized);
|
|
19088
20440
|
let current = parsed.root;
|
|
19089
20441
|
const rel = relative6(parsed.root, normalized);
|
|
19090
20442
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
19091
20443
|
current = join21(current, segment);
|
|
19092
|
-
if (!
|
|
20444
|
+
if (!existsSync19(current))
|
|
19093
20445
|
return null;
|
|
19094
20446
|
if (lstatSync6(current).isSymbolicLink())
|
|
19095
20447
|
return current;
|
|
@@ -19110,7 +20462,7 @@ function packagedInboxSkillPath(explicitPath) {
|
|
|
19110
20462
|
join21(import.meta.dir, "..", "assets", "skills", "inbox", "SKILL.md"),
|
|
19111
20463
|
join21(process.cwd(), "assets", "skills", "inbox", "SKILL.md")
|
|
19112
20464
|
];
|
|
19113
|
-
const found = candidates.find((candidate) =>
|
|
20465
|
+
const found = candidates.find((candidate) => existsSync19(candidate));
|
|
19114
20466
|
if (!found) {
|
|
19115
20467
|
throw new Error(`packaged inbox skill contract is missing (checked ${candidates.length} package-relative locations)`);
|
|
19116
20468
|
}
|
|
@@ -19122,7 +20474,7 @@ function readCanonicalSkill(explicitPath) {
|
|
|
19122
20474
|
if (!stat?.isFile()) {
|
|
19123
20475
|
throw new Error("packaged inbox skill contract is not a regular file");
|
|
19124
20476
|
}
|
|
19125
|
-
const content =
|
|
20477
|
+
const content = readFileSync13(assetPath, "utf8");
|
|
19126
20478
|
if (!content.includes("conversations watch --from <agent> --all")) {
|
|
19127
20479
|
throw new Error("packaged inbox skill contract does not declare the canonical conversations watcher");
|
|
19128
20480
|
}
|
|
@@ -19132,7 +20484,7 @@ function readCanonicalSkill(explicitPath) {
|
|
|
19132
20484
|
return { content, sha256: sha2569(content) };
|
|
19133
20485
|
}
|
|
19134
20486
|
function runProbe(command, args) {
|
|
19135
|
-
const result =
|
|
20487
|
+
const result = spawnSync3(command, args, {
|
|
19136
20488
|
encoding: "utf8",
|
|
19137
20489
|
timeout: 5000,
|
|
19138
20490
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -19159,8 +20511,8 @@ function compareVersions(left, right) {
|
|
|
19159
20511
|
}
|
|
19160
20512
|
return 0;
|
|
19161
20513
|
}
|
|
19162
|
-
function inspectSkillMarkers(
|
|
19163
|
-
return INBOX_SKILL_MARKERS.map((parts) => join21(
|
|
20514
|
+
function inspectSkillMarkers(homeDir6) {
|
|
20515
|
+
return INBOX_SKILL_MARKERS.map((parts) => join21(homeDir6, ...parts)).map((path) => {
|
|
19164
20516
|
const stat = lstatOrNull(path);
|
|
19165
20517
|
if (!stat)
|
|
19166
20518
|
return null;
|
|
@@ -19169,16 +20521,16 @@ function inspectSkillMarkers(homeDir4) {
|
|
|
19169
20521
|
}
|
|
19170
20522
|
return {
|
|
19171
20523
|
path,
|
|
19172
|
-
content:
|
|
20524
|
+
content: readFileSync13(path, "utf8"),
|
|
19173
20525
|
mode: stat.mode & 511,
|
|
19174
20526
|
regular: true
|
|
19175
20527
|
};
|
|
19176
20528
|
}).filter((snapshot) => snapshot !== null);
|
|
19177
20529
|
}
|
|
19178
20530
|
function inspectInbox(options) {
|
|
19179
|
-
const
|
|
20531
|
+
const homeDir6 = options.homeDir ?? homedir11();
|
|
19180
20532
|
const runtimeCommand = options.conversationsCommand ?? "conversations";
|
|
19181
|
-
const snapshots = inspectSkillMarkers(
|
|
20533
|
+
const snapshots = inspectSkillMarkers(homeDir6);
|
|
19182
20534
|
const skillPresent = snapshots.length > 0;
|
|
19183
20535
|
let canonicalContent = null;
|
|
19184
20536
|
let canonicalSha256 = null;
|
|
@@ -19204,7 +20556,7 @@ function inspectInbox(options) {
|
|
|
19204
20556
|
let reason = "skill not installed";
|
|
19205
20557
|
if (skillPresent) {
|
|
19206
20558
|
const nonRegular = snapshots.some((snapshot) => !snapshot.regular);
|
|
19207
|
-
const symlinkAncestor = snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(
|
|
20559
|
+
const symlinkAncestor = snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname9(path))).find((found) => found !== null);
|
|
19208
20560
|
if (nonRegular)
|
|
19209
20561
|
reason = "managed skill target is not a regular file";
|
|
19210
20562
|
else if (symlinkAncestor)
|
|
@@ -19291,10 +20643,10 @@ function cleanup(path) {
|
|
|
19291
20643
|
rmSync5(path, { force: true });
|
|
19292
20644
|
}
|
|
19293
20645
|
function writeAtomic(path, content, mode) {
|
|
19294
|
-
assertNoSymlinkAncestors3(
|
|
20646
|
+
assertNoSymlinkAncestors3(dirname9(path));
|
|
19295
20647
|
const tempPath = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
19296
20648
|
try {
|
|
19297
|
-
mkdirSync8(
|
|
20649
|
+
mkdirSync8(dirname9(path), { recursive: true, mode: 493 });
|
|
19298
20650
|
writeFileSync5(tempPath, content, { mode, flag: "wx" });
|
|
19299
20651
|
renameSync2(tempPath, path);
|
|
19300
20652
|
} finally {
|
|
@@ -19303,7 +20655,7 @@ function writeAtomic(path, content, mode) {
|
|
|
19303
20655
|
}
|
|
19304
20656
|
var DEFAULT_SKILL_WRITE_FILE_OPERATIONS = {
|
|
19305
20657
|
lstat: lstatOrNull,
|
|
19306
|
-
read: (path) =>
|
|
20658
|
+
read: (path) => readFileSync13(path, "utf8"),
|
|
19307
20659
|
write: writeAtomic
|
|
19308
20660
|
};
|
|
19309
20661
|
function writeSkillContractsTransactional(snapshots, canonicalContent, fileOperations = DEFAULT_SKILL_WRITE_FILE_OPERATIONS) {
|
|
@@ -19370,7 +20722,7 @@ async function reconcileManagedSkillRuntimes(options = {}) {
|
|
|
19370
20722
|
dry_run: dryRun
|
|
19371
20723
|
};
|
|
19372
20724
|
}
|
|
19373
|
-
const symlinkedAncestor = before.snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(
|
|
20725
|
+
const symlinkedAncestor = before.snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname9(path))).find((found) => found !== null);
|
|
19374
20726
|
if (symlinkedAncestor) {
|
|
19375
20727
|
return {
|
|
19376
20728
|
runtimes: [{ ...status, action: "failed", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
@@ -19453,28 +20805,28 @@ init_project_context();
|
|
|
19453
20805
|
init_config_store();
|
|
19454
20806
|
init_apply();
|
|
19455
20807
|
init_config_agents();
|
|
19456
|
-
import { existsSync as
|
|
20808
|
+
import { existsSync as existsSync21, readFileSync as readFileSync15 } from "fs";
|
|
19457
20809
|
|
|
19458
20810
|
// src/lib/package-version.ts
|
|
19459
|
-
import { existsSync as
|
|
19460
|
-
import { dirname as
|
|
20811
|
+
import { existsSync as existsSync20, readFileSync as readFileSync14 } from "fs";
|
|
20812
|
+
import { dirname as dirname10, join as join22 } from "path";
|
|
19461
20813
|
import { fileURLToPath } from "url";
|
|
19462
20814
|
var cached = null;
|
|
19463
20815
|
function getPackageVersion() {
|
|
19464
20816
|
if (cached)
|
|
19465
20817
|
return cached;
|
|
19466
20818
|
try {
|
|
19467
|
-
let dir =
|
|
20819
|
+
let dir = dirname10(fileURLToPath(import.meta.url));
|
|
19468
20820
|
for (let i = 0;i < 8; i++) {
|
|
19469
20821
|
const pkgPath = join22(dir, "package.json");
|
|
19470
|
-
if (
|
|
19471
|
-
const pkg = JSON.parse(
|
|
20822
|
+
if (existsSync20(pkgPath)) {
|
|
20823
|
+
const pkg = JSON.parse(readFileSync14(pkgPath, "utf8"));
|
|
19472
20824
|
if (pkg.name === "@hasna/instructions" && pkg.version) {
|
|
19473
20825
|
cached = pkg.version;
|
|
19474
20826
|
return cached;
|
|
19475
20827
|
}
|
|
19476
20828
|
}
|
|
19477
|
-
const parent =
|
|
20829
|
+
const parent = dirname10(dir);
|
|
19478
20830
|
if (parent === dir)
|
|
19479
20831
|
break;
|
|
19480
20832
|
dir = parent;
|
|
@@ -19531,11 +20883,11 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
|
|
|
19531
20883
|
continue;
|
|
19532
20884
|
knownTargets += 1;
|
|
19533
20885
|
const targetPath = expandPath(config.target_path);
|
|
19534
|
-
if (!
|
|
20886
|
+
if (!existsSync21(targetPath)) {
|
|
19535
20887
|
missingTargets += 1;
|
|
19536
20888
|
continue;
|
|
19537
20889
|
}
|
|
19538
|
-
const disk =
|
|
20890
|
+
const disk = readFileSync15(targetPath, "utf-8");
|
|
19539
20891
|
const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(config.target_path, config.format));
|
|
19540
20892
|
if (redactedDisk !== config.content) {
|
|
19541
20893
|
driftedTargets += 1;
|
|
@@ -19631,7 +20983,7 @@ init_config_store();
|
|
|
19631
20983
|
|
|
19632
20984
|
// src/lib/provider-context.ts
|
|
19633
20985
|
import { createHash as createHash10 } from "crypto";
|
|
19634
|
-
import { existsSync as
|
|
20986
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync9, readFileSync as readFileSync16, writeFileSync as writeFileSync6 } from "fs";
|
|
19635
20987
|
import { join as join23 } from "path";
|
|
19636
20988
|
var PROVIDER_CONTEXT_DIR = ".hasna/provider-context";
|
|
19637
20989
|
var PROVIDER_CONTEXT_MANIFEST = "manifest.json";
|
|
@@ -19778,7 +21130,7 @@ function resolveAndRenderProviderContext(opts) {
|
|
|
19778
21130
|
const reason = entry === null && opts.rawEndpoint ? originAccepted ? `endpoint "${recordedEndpoint}" is not in the provider-context registry; using the invariant fragment` : "endpoint rejected (embedded credentials or unparseable); using the invariant fragment" : null;
|
|
19779
21131
|
const content = renderProviderFragment(entry);
|
|
19780
21132
|
const dir = join23(opts.homeDir, PROVIDER_CONTEXT_DIR);
|
|
19781
|
-
if (!
|
|
21133
|
+
if (!existsSync22(dir))
|
|
19782
21134
|
mkdirSync9(dir, { recursive: true });
|
|
19783
21135
|
const filename = `${entry ? entry.key : "invariant"}.md`;
|
|
19784
21136
|
const fragmentPath2 = join23(dir, filename);
|
|
@@ -19787,8 +21139,8 @@ function resolveAndRenderProviderContext(opts) {
|
|
|
19787
21139
|
const manifestPath = join23(dir, PROVIDER_CONTEXT_MANIFEST);
|
|
19788
21140
|
let manifest = { schema: PROVIDER_CONTEXT_SCHEMA, fragments: {} };
|
|
19789
21141
|
try {
|
|
19790
|
-
if (
|
|
19791
|
-
const parsed = JSON.parse(
|
|
21142
|
+
if (existsSync22(manifestPath)) {
|
|
21143
|
+
const parsed = JSON.parse(readFileSync16(manifestPath, "utf8"));
|
|
19792
21144
|
if (parsed && typeof parsed === "object")
|
|
19793
21145
|
manifest = parsed;
|
|
19794
21146
|
}
|
|
@@ -19826,8 +21178,8 @@ function providerContextAuditLine(r, nowIso = new Date().toISOString()) {
|
|
|
19826
21178
|
}
|
|
19827
21179
|
|
|
19828
21180
|
// src/cli/index.tsx
|
|
19829
|
-
import { createRequire } from "module";
|
|
19830
|
-
var pkg =
|
|
21181
|
+
import { createRequire as createRequire3 } from "module";
|
|
21182
|
+
var pkg = createRequire3(import.meta.url)("../../package.json");
|
|
19831
21183
|
var EAGAIN_SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
19832
21184
|
function writeStdout(text) {
|
|
19833
21185
|
const buf = Buffer.from(text, "utf8");
|
|
@@ -19931,7 +21283,7 @@ function parseSessionSource(value, order) {
|
|
|
19931
21283
|
if (!path)
|
|
19932
21284
|
throw new Error(`Invalid --source "${value}" (expected path or id=path)`);
|
|
19933
21285
|
const absPath = resolveSessionPath(path);
|
|
19934
|
-
if (!
|
|
21286
|
+
if (!existsSync24(absPath))
|
|
19935
21287
|
throw new Error(`Instruction source file not found: ${absPath}`);
|
|
19936
21288
|
const content = readSessionInstructionSourceFile(absPath);
|
|
19937
21289
|
const source = sourceFromFilePath(absPath, content, order);
|
|
@@ -19985,7 +21337,7 @@ function readSessionInstructionSourceFile(path) {
|
|
|
19985
21337
|
if (stat.size > SESSION_MANAGED_INPUT_MAX_BYTES) {
|
|
19986
21338
|
throw new Error(`SESSION_SOURCE_INPUT_TOO_LARGE: instruction source file exceeds ${SESSION_MANAGED_INPUT_MAX_BYTES} bytes`);
|
|
19987
21339
|
}
|
|
19988
|
-
return
|
|
21340
|
+
return readFileSync18(path, "utf-8");
|
|
19989
21341
|
}
|
|
19990
21342
|
function parseLayeredReference(value) {
|
|
19991
21343
|
const trimmed = value.trim();
|
|
@@ -20012,9 +21364,9 @@ async function collectSessionSources(opts, tool, store) {
|
|
|
20012
21364
|
}
|
|
20013
21365
|
for (const value of opts.identityExport ?? []) {
|
|
20014
21366
|
const path = resolveSessionPath(value);
|
|
20015
|
-
if (!
|
|
21367
|
+
if (!existsSync24(path))
|
|
20016
21368
|
throw new Error(`Identity instruction export not found: ${path}`);
|
|
20017
|
-
const parsed = JSON.parse(
|
|
21369
|
+
const parsed = JSON.parse(readFileSync18(path, "utf-8"));
|
|
20018
21370
|
sources.push(...sourcesFromIdentityExport(parsed, { path, tool, orderOffset: sources.length }));
|
|
20019
21371
|
}
|
|
20020
21372
|
return sources.map((source) => {
|
|
@@ -20146,7 +21498,7 @@ function readProjectContextBundleOption(value, allowMissing = false) {
|
|
|
20146
21498
|
if (value === "-")
|
|
20147
21499
|
return { json: readBoundedProjectContextStdin() };
|
|
20148
21500
|
const path = resolveSessionPath(value);
|
|
20149
|
-
if (!
|
|
21501
|
+
if (!existsSync24(path)) {
|
|
20150
21502
|
if (allowMissing)
|
|
20151
21503
|
return {};
|
|
20152
21504
|
throw new ProjectContextError("PROJECT_CONTEXT_INPUT_MISSING", `bundle file not found: ${path}`);
|
|
@@ -20158,7 +21510,7 @@ function readProjectContextBundleOption(value, allowMissing = false) {
|
|
|
20158
21510
|
if (stat.size > PROJECT_CONTEXT_MAX_INPUT_BYTES) {
|
|
20159
21511
|
throw new ProjectContextError("PROJECT_CONTEXT_INPUT_TOO_LARGE", `bundle exceeds ${PROJECT_CONTEXT_MAX_INPUT_BYTES} bytes`);
|
|
20160
21512
|
}
|
|
20161
|
-
return { json:
|
|
21513
|
+
return { json: readFileSync18(path, "utf8"), sourcePath: path };
|
|
20162
21514
|
}
|
|
20163
21515
|
function readBoundedProjectContextStdin() {
|
|
20164
21516
|
const chunks = [];
|
|
@@ -20317,16 +21669,16 @@ program.command("tag <id>").description("Add or remove tags on a stored config (
|
|
|
20317
21669
|
console.log(chalk.green("\u2713") + ` Tags on ${chalk.bold(updated.name)} ${chalk.dim(`(${updated.slug})`)}: ${nextTags.join(", ") || chalk.dim("(none)")}`);
|
|
20318
21670
|
});
|
|
20319
21671
|
program.command("add <path>").description("Ingest a file into the config DB").option("-n, --name <name>", "config name (defaults to filename)").option("-c, --category <cat>", "category override").option("-a, --agent <agent>", "agent override").option("-k, --kind <kind>", "kind: file|reference", "file").option("--template", "mark as template (has {{VAR}} placeholders)").option("--update", "if a config already owns this path, update that row in place instead of refusing").action(async (filePath, opts) => {
|
|
20320
|
-
const abs =
|
|
20321
|
-
if (!
|
|
21672
|
+
const abs = resolve16(filePath);
|
|
21673
|
+
if (!existsSync24(abs)) {
|
|
20322
21674
|
console.error(chalk.red(`File not found: ${abs}`));
|
|
20323
21675
|
process.exit(1);
|
|
20324
21676
|
}
|
|
20325
|
-
const rawContent =
|
|
21677
|
+
const rawContent = readFileSync18(abs, "utf-8");
|
|
20326
21678
|
const storedFmt = detectFormat(abs);
|
|
20327
21679
|
const fmt = redactFormatForTarget(abs, storedFmt);
|
|
20328
21680
|
const { content, redacted, isTemplate: isTemplate2 } = redactContent(rawContent, fmt);
|
|
20329
|
-
const targetPath = abs.startsWith(
|
|
21681
|
+
const targetPath = abs.startsWith(homedir13()) ? abs.replace(homedir13(), "~") : abs;
|
|
20330
21682
|
const name = opts.name || filePath.split("/").pop();
|
|
20331
21683
|
const store = resolveConfigStore();
|
|
20332
21684
|
const allConfigs = await store.listConfigs();
|
|
@@ -20494,9 +21846,9 @@ program.command("sync").description("Sync known AI coding configs from disk into
|
|
|
20494
21846
|
if (opts.project) {
|
|
20495
21847
|
const dir = typeof opts.project === "string" ? opts.project : process.cwd();
|
|
20496
21848
|
if (opts.all) {
|
|
20497
|
-
const { readdirSync:
|
|
21849
|
+
const { readdirSync: readdirSync7 } = await import("fs");
|
|
20498
21850
|
const absDir = expandPath(dir);
|
|
20499
|
-
const entries =
|
|
21851
|
+
const entries = readdirSync7(absDir, { withFileTypes: true });
|
|
20500
21852
|
let totalAdded = 0, totalUpdated = 0, totalUnchanged = 0, projects = 0;
|
|
20501
21853
|
for (const entry of entries) {
|
|
20502
21854
|
if (!entry.isDirectory())
|
|
@@ -20514,7 +21866,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
|
|
|
20514
21866
|
".aicopilot",
|
|
20515
21867
|
".cursor",
|
|
20516
21868
|
".agents"
|
|
20517
|
-
].some((marker) =>
|
|
21869
|
+
].some((marker) => existsSync24(join25(projDir, marker)));
|
|
20518
21870
|
if (!hasAgentConfig)
|
|
20519
21871
|
continue;
|
|
20520
21872
|
const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun, store });
|
|
@@ -20565,10 +21917,10 @@ program.command("import <file>").description("Import configs from a tar.gz bundl
|
|
|
20565
21917
|
});
|
|
20566
21918
|
program.command("whoami").description("Show setup summary").action(async () => {
|
|
20567
21919
|
const store = resolveConfigStore();
|
|
20568
|
-
const dbPath =
|
|
21920
|
+
const dbPath = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join25(getRawStoreRoot(), "instructions.db");
|
|
20569
21921
|
const stats = await store.getConfigStats();
|
|
20570
21922
|
console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
|
|
20571
|
-
console.log(chalk.cyan(
|
|
21923
|
+
console.log(chalk.cyan(store.mode === "api" ? "API:" : "DB:") + " " + dbPath);
|
|
20572
21924
|
console.log(chalk.cyan("Total configs:") + " " + (stats["total"] || 0));
|
|
20573
21925
|
console.log();
|
|
20574
21926
|
console.log(chalk.bold("By category:"));
|
|
@@ -21444,14 +22796,14 @@ mcpCmd.command("install").alias("add").description("Install configs MCP server i
|
|
|
21444
22796
|
} else if (target === "codex") {
|
|
21445
22797
|
const { appendFileSync, existsSync: ex } = await import("fs");
|
|
21446
22798
|
const { join: j } = await import("path");
|
|
21447
|
-
const configPath = j(
|
|
22799
|
+
const configPath = j(homedir13(), ".codex", "config.toml");
|
|
21448
22800
|
const block = `
|
|
21449
22801
|
[mcp_servers.configs]
|
|
21450
22802
|
command = "${mcpBinary}"
|
|
21451
22803
|
args = []
|
|
21452
22804
|
`;
|
|
21453
22805
|
if (ex(configPath)) {
|
|
21454
|
-
const content =
|
|
22806
|
+
const content = readFileSync18(configPath, "utf-8");
|
|
21455
22807
|
if (content.includes("[mcp_servers.configs]")) {
|
|
21456
22808
|
console.log(chalk.dim("= Already installed in Codex"));
|
|
21457
22809
|
continue;
|
|
@@ -21462,7 +22814,7 @@ args = []
|
|
|
21462
22814
|
} else if (target === "antigravity") {
|
|
21463
22815
|
const { mkdirSync: md, readFileSync: rf, writeFileSync: wf, existsSync: ex } = await import("fs");
|
|
21464
22816
|
const { dirname: dn, join: j } = await import("path");
|
|
21465
|
-
const configPath = j(
|
|
22817
|
+
const configPath = j(homedir13(), ".gemini", "config", "mcp_config.json");
|
|
21466
22818
|
let settings = {};
|
|
21467
22819
|
if (ex(configPath)) {
|
|
21468
22820
|
try {
|
|
@@ -21546,9 +22898,9 @@ DB stats:`));
|
|
|
21546
22898
|
if (count > 0)
|
|
21547
22899
|
console.log(` ${key.padEnd(18)} ${count}`);
|
|
21548
22900
|
}
|
|
21549
|
-
const location =
|
|
22901
|
+
const location = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join25(getRawStoreRoot(), "instructions.db");
|
|
21550
22902
|
console.log(chalk.dim(`
|
|
21551
|
-
${
|
|
22903
|
+
${store.mode === "api" ? "API" : "DB"}: ${location}`));
|
|
21552
22904
|
});
|
|
21553
22905
|
program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").option("--json", "output metadata-only JSON").action(async (opts) => {
|
|
21554
22906
|
const status = await getConfigsStatus(resolveConfigStore());
|
|
@@ -21638,9 +22990,9 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
|
|
|
21638
22990
|
console.log(chalk.cyan("Known files on disk:"));
|
|
21639
22991
|
for (const k of KNOWN_CONFIGS) {
|
|
21640
22992
|
if (k.rulesDir) {
|
|
21641
|
-
|
|
22993
|
+
existsSync24(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail2(`${k.rulesDir}/ not found`);
|
|
21642
22994
|
} else {
|
|
21643
|
-
|
|
22995
|
+
existsSync24(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail2(`${k.path} not found`);
|
|
21644
22996
|
}
|
|
21645
22997
|
}
|
|
21646
22998
|
const allConfigs = await store.listConfigs();
|
|
@@ -21794,16 +23146,16 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
21794
23146
|
for (const k of KNOWN_CONFIGS) {
|
|
21795
23147
|
if (k.rulesDir) {
|
|
21796
23148
|
const absDir = expandPath2(k.rulesDir);
|
|
21797
|
-
if (!
|
|
23149
|
+
if (!existsSync24(absDir))
|
|
21798
23150
|
continue;
|
|
21799
|
-
const { readdirSync:
|
|
21800
|
-
for (const f of
|
|
23151
|
+
const { readdirSync: readdirSync7 } = await import("fs");
|
|
23152
|
+
for (const f of readdirSync7(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
21801
23153
|
const abs = join25(absDir, f);
|
|
21802
23154
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
21803
23155
|
}
|
|
21804
23156
|
} else {
|
|
21805
23157
|
const abs = expandPath2(k.path);
|
|
21806
|
-
if (
|
|
23158
|
+
if (existsSync24(abs))
|
|
21807
23159
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
21808
23160
|
}
|
|
21809
23161
|
}
|
|
@@ -21811,7 +23163,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
21811
23163
|
const tick = async () => {
|
|
21812
23164
|
let changed = 0;
|
|
21813
23165
|
for (const [abs, oldMtime] of mtimes) {
|
|
21814
|
-
if (!
|
|
23166
|
+
if (!existsSync24(abs))
|
|
21815
23167
|
continue;
|
|
21816
23168
|
const newMtime = st(abs).mtimeMs;
|
|
21817
23169
|
if (newMtime !== oldMtime) {
|
|
@@ -21823,7 +23175,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
21823
23175
|
for (const k of KNOWN_CONFIGS) {
|
|
21824
23176
|
if (k.rulesDir) {
|
|
21825
23177
|
const absDir = expandPath2(k.rulesDir);
|
|
21826
|
-
if (!
|
|
23178
|
+
if (!existsSync24(absDir))
|
|
21827
23179
|
continue;
|
|
21828
23180
|
for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
21829
23181
|
const abs = join25(absDir, f);
|
|
@@ -21834,7 +23186,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
21834
23186
|
}
|
|
21835
23187
|
} else {
|
|
21836
23188
|
const abs = expandPath2(k.path);
|
|
21837
|
-
if (
|
|
23189
|
+
if (existsSync24(abs) && !mtimes.has(abs)) {
|
|
21838
23190
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
21839
23191
|
changed++;
|
|
21840
23192
|
}
|
|
@@ -21862,11 +23214,11 @@ program.command("report").description("Summary of stored configs, drift, and eco
|
|
|
21862
23214
|
if (!c.target_path)
|
|
21863
23215
|
continue;
|
|
21864
23216
|
const abs = expandPath(c.target_path);
|
|
21865
|
-
if (!
|
|
23217
|
+
if (!existsSync24(abs)) {
|
|
21866
23218
|
missing++;
|
|
21867
23219
|
continue;
|
|
21868
23220
|
}
|
|
21869
|
-
const disk =
|
|
23221
|
+
const disk = readFileSync18(abs, "utf-8");
|
|
21870
23222
|
const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(c.target_path, c.format));
|
|
21871
23223
|
if (redactedDisk !== c.content)
|
|
21872
23224
|
drifted++;
|
|
@@ -21933,7 +23285,7 @@ program.command("clean").description("Remove configs from DB whose target files
|
|
|
21933
23285
|
if (!c.target_path)
|
|
21934
23286
|
continue;
|
|
21935
23287
|
const abs = expandPath(c.target_path);
|
|
21936
|
-
if (!
|
|
23288
|
+
if (!existsSync24(abs)) {
|
|
21937
23289
|
if (printed < maxPrinted) {
|
|
21938
23290
|
if (opts.dryRun) {
|
|
21939
23291
|
console.log(chalk.yellow(" would remove:") + ` ${c.slug} ${chalk.dim(`(${truncateMiddle(c.target_path, 88)})`)}`);
|
|
@@ -21966,12 +23318,9 @@ program.command("bootstrap").description("Install the full @hasna ecosystem: CLI
|
|
|
21966
23318
|
{ name: "@hasna/skills", bin: "skills", mcp: "skills-mcp" },
|
|
21967
23319
|
{ name: "@hasna/economy", bin: "economy", mcp: "economy-mcp" },
|
|
21968
23320
|
{ name: "@hasna/attachments", bin: "attachments", mcp: "attachments-mcp" },
|
|
21969
|
-
{ name: "@hasna/sessions", bin: "sessions", mcp: "sessions-mcp" },
|
|
21970
23321
|
{ name: "@hasna/emails", bin: "emails", mcp: "emails-mcp" },
|
|
21971
23322
|
{ name: "@hasna/recordings", bin: "recordings", mcp: "recordings-mcp" },
|
|
21972
|
-
{ name: "@hasna/
|
|
21973
|
-
{ name: "@hasna/assistants", bin: "assistants", mcp: "assistants-mcp" },
|
|
21974
|
-
{ name: "@hasna/brains", bin: "brains", mcp: "brains-mcp" }
|
|
23323
|
+
{ name: "@hasna/assistants", bin: "assistants", mcp: "assistants-mcp" }
|
|
21975
23324
|
];
|
|
21976
23325
|
console.log(chalk.bold("@hasna/instructions bootstrap") + chalk.dim(` \u2014 installing ${packages.length} ecosystem packages
|
|
21977
23326
|
`));
|
|
@@ -22079,13 +23428,13 @@ providerContextCmd.command("resolve").description("Resolve the endpoint to a pro
|
|
|
22079
23428
|
try {
|
|
22080
23429
|
const rawEndpoint = opts.endpoint ?? process.env["ANTHROPIC_BASE_URL"] ?? "";
|
|
22081
23430
|
const rawModel = opts.model ?? process.env["ANTHROPIC_MODEL"] ?? "";
|
|
22082
|
-
const
|
|
23431
|
+
const homeDir6 = opts.home ?? homedir13();
|
|
22083
23432
|
const origin = normalizeEndpointOrigin(rawEndpoint);
|
|
22084
23433
|
const resolution = resolveAndRenderProviderContext({
|
|
22085
23434
|
origin,
|
|
22086
23435
|
rawEndpoint,
|
|
22087
23436
|
rawModel,
|
|
22088
|
-
homeDir:
|
|
23437
|
+
homeDir: homeDir6
|
|
22089
23438
|
});
|
|
22090
23439
|
if (opts.json) {
|
|
22091
23440
|
printJson({
|