@hasna/instructions 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -10
- package/dist/chunks/{apply-ynfn1av7.js → apply-c3sd4agz.js} +5 -3
- package/dist/chunks/{apply-sfzja1f3.js → apply-pnn7a3y7.js} +6 -3
- package/dist/chunks/database-wstmr56z.js +23 -0
- package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
- package/dist/chunks/index-5d7zdftc.js +186 -0
- package/dist/chunks/{index-1hpwskwd.js → index-6jncnnw7.js} +318 -329
- package/dist/chunks/index-8rb7ng2y.js +148 -0
- package/dist/chunks/{index-v7dkb384.js → index-aj66f6xr.js} +243 -100
- package/dist/chunks/index-fpeyzn3n.js +186 -0
- package/dist/chunks/{index-9p9gn7gh.js → index-jg8ptcwh.js} +5 -7
- package/dist/chunks/index-mvckkf15.js +363 -0
- package/dist/chunks/index-n3fjspr0.js +119 -0
- package/dist/chunks/{index-85n5wnd8.js → index-pdpg8a44.js} +5 -7
- package/dist/chunks/{index-e9epbsvs.js → index-r7nfef2g.js} +15 -4
- package/dist/chunks/{index-axhwg61p.js → index-ypcef3a2.js} +15 -4
- package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
- package/dist/chunks/{local-a9nen65z.js → local-0a9gja58.js} +142 -2
- package/dist/chunks/{local-0d8qrc92.js → local-969ye538.js} +153 -188
- package/dist/chunks/s3-backup-hxe3zpfz.js +240 -0
- package/dist/chunks/s3-config-hhght91k.js +14 -0
- package/dist/chunks/s3-object-store-n0s82swt.js +14 -0
- package/dist/chunks/{sync-vy1yjvs6.js → sync-q5hvwq6e.js} +4 -4
- package/dist/chunks/{sync-m46fc257.js → sync-xtkbm5br.js} +5 -4
- package/dist/chunks/{template-02wjvv05.js → template-fsfxpe4p.js} +1 -1
- package/dist/chunks/{template-ke972qqe.js → template-wgnzdn7h.js} +1 -1
- package/dist/cli/index.js +1236 -256
- package/dist/data/config-store.d.ts +10 -1
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/db/configs.d.ts +4 -1
- package/dist/db/configs.d.ts.map +1 -1
- package/dist/db/local.d.ts +1 -1
- package/dist/db/local.d.ts.map +1 -1
- package/dist/index.js +1086 -215
- package/dist/lib/apply.d.ts +7 -0
- package/dist/lib/apply.d.ts.map +1 -1
- package/dist/lib/compact-output.d.ts +3 -18
- package/dist/lib/compact-output.d.ts.map +1 -1
- package/dist/lib/export.d.ts +22 -3
- package/dist/lib/export.d.ts.map +1 -1
- package/dist/lib/import.d.ts +18 -0
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/legacy-store-migration.d.ts +39 -0
- package/dist/lib/legacy-store-migration.d.ts.map +1 -0
- package/dist/lib/managed-skill-runtimes.d.ts +1 -0
- package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
- package/dist/lib/sync-dir.d.ts.map +1 -1
- package/dist/mcp/index.js +41 -30
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +229 -10
- package/dist/sdk/resolve.d.ts +1 -1
- package/dist/sdk/resolve.d.ts.map +1 -1
- package/dist/sdk/v1-client.d.ts +58 -0
- package/dist/sdk/v1-client.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +242 -21
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts +13 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1261 -427
- package/dist/server/openapi.d.ts +1379 -93
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts +25 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/cloud-store.d.ts +45 -2
- package/dist/storage/cloud-store.d.ts.map +1 -1
- package/dist/storage/index.d.ts +7 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +744 -0
- package/dist/storage/s3-backup.d.ts +76 -0
- package/dist/storage/s3-backup.d.ts.map +1 -0
- package/dist/storage/s3-config.d.ts +46 -0
- package/dist/storage/s3-config.d.ts.map +1 -0
- package/dist/storage/s3-object-store.d.ts +70 -0
- package/dist/storage/s3-object-store.d.ts.map +1 -0
- package/dist/types/index.d.ts +98 -3
- package/dist/types/index.d.ts.map +1 -1
- package/hasna.contract.json +110 -0
- package/migrations/0001_instructions.sql +63 -0
- package/migrations/0002_api_keys.sql +20 -0
- package/migrations/0003_idempotency_receipts.sql +24 -0
- package/package.json +24 -13
- package/assets/skills/inbox/SKILL.md +0 -86
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ var __export = (target, all) => {
|
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
18
|
// src/types/index.ts
|
|
19
|
-
var CONFIG_KINDS, CONFIG_CATEGORIES, CONFIG_AGENTS, CONFIG_TRANSFORMS, CONFIG_FORMATS, PROFILE_CONFIG_BINDING_SCHEMA = "hasna.instructions.profile-config-binding/v1", INSTRUCTION_ACTIVATION_MODES, INSTRUCTION_FALLBACKS, PROFILE_ASSET_BINDING_SCHEMA = "hasna.instructions.profile-asset-binding/v1", ASSET_KINDS, ASSET_DESTINATION_STRATEGIES, ASSET_SCOPES, ASSET_UNINSTALL_POLICIES, ASSET_ROLLBACK_POLICIES, ConfigNotFoundError, ProfileNotFoundError, ConfigApplyError, TemplateRenderError;
|
|
19
|
+
var CONFIG_KINDS, CONFIG_CATEGORIES, CONFIG_AGENTS, CONFIG_TRANSFORMS, CONFIG_FORMATS, PROFILE_CONFIG_BINDING_SCHEMA = "hasna.instructions.profile-config-binding/v1", INSTRUCTION_ACTIVATION_MODES, INSTRUCTION_FALLBACKS, PROFILE_ASSET_BINDING_SCHEMA = "hasna.instructions.profile-asset-binding/v1", ASSET_KINDS, ASSET_DESTINATION_STRATEGIES, ASSET_SCOPES, ASSET_UNINSTALL_POLICIES, ASSET_ROLLBACK_POLICIES, INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA = "hasna.instructions.domain-archive/v2", ConfigNotFoundError, ProfileNotFoundError, ConfigApplyError, TemplateRenderError;
|
|
20
20
|
var init_types = __esm(() => {
|
|
21
21
|
CONFIG_KINDS = ["file", "reference"];
|
|
22
22
|
CONFIG_CATEGORIES = [
|
|
@@ -108,9 +108,9 @@ function parseCursor(value) {
|
|
|
108
108
|
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
109
109
|
if (!Number.isFinite(parsed) || parsed < 0)
|
|
110
110
|
return 0;
|
|
111
|
-
return Math.floor(parsed);
|
|
111
|
+
return Math.min(Math.floor(parsed), MAX_LIST_CURSOR);
|
|
112
112
|
}
|
|
113
|
-
var DEFAULT_LIST_LIMIT = 20, MAX_LIST_LIMIT = 100;
|
|
113
|
+
var DEFAULT_LIST_LIMIT = 20, MAX_LIST_LIMIT = 100, MAX_LIST_CURSOR = 1e5;
|
|
114
114
|
|
|
115
115
|
// src/lib/bounded-read.ts
|
|
116
116
|
function normalizeBoundedReadOptions(options = {}) {
|
|
@@ -10925,7 +10925,7 @@ var init_instruction_graph = __esm(() => {
|
|
|
10925
10925
|
};
|
|
10926
10926
|
});
|
|
10927
10927
|
|
|
10928
|
-
//
|
|
10928
|
+
// ../../node_modules/.bun/@hasna+contracts@1.0.2+e8014c875821e0be/node_modules/@hasna/contracts/dist/client/transport.js
|
|
10929
10929
|
import { isIP } from "net";
|
|
10930
10930
|
import { spawnSync } from "child_process";
|
|
10931
10931
|
import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync5 } from "fs";
|
|
@@ -11658,7 +11658,7 @@ var init_transport = __esm(() => {
|
|
|
11658
11658
|
]);
|
|
11659
11659
|
});
|
|
11660
11660
|
|
|
11661
|
-
//
|
|
11661
|
+
// ../../node_modules/.bun/@hasna+contracts@1.0.2+e8014c875821e0be/node_modules/@hasna/contracts/dist/client/storage.js
|
|
11662
11662
|
import { isIP as isIP2 } from "net";
|
|
11663
11663
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
11664
11664
|
import { closeSync as closeSync3, fstatSync as fstatSync3, openSync as openSync3, readFileSync as readFileSync6 } from "fs";
|
|
@@ -13326,6 +13326,143 @@ function listConfigs(filter, db) {
|
|
|
13326
13326
|
const rows = d.query(`SELECT * FROM configs ${where} ORDER BY category, name`).all(...params);
|
|
13327
13327
|
return rows.map(rowToConfig);
|
|
13328
13328
|
}
|
|
13329
|
+
function listConfigsPage(filter = {}, options = {}, db) {
|
|
13330
|
+
const d = db || getDatabase();
|
|
13331
|
+
const conditions = [];
|
|
13332
|
+
const params = [];
|
|
13333
|
+
if (filter.category) {
|
|
13334
|
+
conditions.push("category = ?");
|
|
13335
|
+
params.push(filter.category);
|
|
13336
|
+
}
|
|
13337
|
+
if (filter.agent) {
|
|
13338
|
+
conditions.push("agent = ?");
|
|
13339
|
+
params.push(filter.agent);
|
|
13340
|
+
}
|
|
13341
|
+
if (filter.kind) {
|
|
13342
|
+
conditions.push("kind = ?");
|
|
13343
|
+
params.push(filter.kind);
|
|
13344
|
+
}
|
|
13345
|
+
if (filter.is_template !== undefined) {
|
|
13346
|
+
conditions.push("is_template = ?");
|
|
13347
|
+
params.push(filter.is_template ? 1 : 0);
|
|
13348
|
+
}
|
|
13349
|
+
if (filter.search) {
|
|
13350
|
+
conditions.push("(name LIKE ? OR description LIKE ? OR content LIKE ?)");
|
|
13351
|
+
const q = `%${filter.search}%`;
|
|
13352
|
+
params.push(q, q, q);
|
|
13353
|
+
}
|
|
13354
|
+
if (filter.tags?.length) {
|
|
13355
|
+
conditions.push(`(${filter.tags.map(() => "tags LIKE ?").join(" AND ")})`);
|
|
13356
|
+
for (const tag of filter.tags)
|
|
13357
|
+
params.push(`%"${tag}"%`);
|
|
13358
|
+
}
|
|
13359
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
13360
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
13361
|
+
const total = d.query(`SELECT COUNT(*) AS total FROM configs ${where}`).get(...params)?.total ?? 0;
|
|
13362
|
+
const pageParams = [...params, normalized.limit, normalized.cursor];
|
|
13363
|
+
const rows = d.query(`SELECT * FROM configs ${where} ORDER BY id LIMIT ? OFFSET ?`).all(...pageParams);
|
|
13364
|
+
return boundedReadPage(rows.map(rowToConfig), total, normalized);
|
|
13365
|
+
}
|
|
13366
|
+
function listConfigIdentitiesPage(filter = {}, options = {}, db) {
|
|
13367
|
+
const d = db || getDatabase();
|
|
13368
|
+
const conditions = [];
|
|
13369
|
+
const params = [];
|
|
13370
|
+
if (filter.category) {
|
|
13371
|
+
conditions.push("category = ?");
|
|
13372
|
+
params.push(filter.category);
|
|
13373
|
+
}
|
|
13374
|
+
if (filter.agent) {
|
|
13375
|
+
conditions.push("agent = ?");
|
|
13376
|
+
params.push(filter.agent);
|
|
13377
|
+
}
|
|
13378
|
+
if (filter.kind) {
|
|
13379
|
+
conditions.push("kind = ?");
|
|
13380
|
+
params.push(filter.kind);
|
|
13381
|
+
}
|
|
13382
|
+
if (filter.is_template !== undefined) {
|
|
13383
|
+
conditions.push("is_template = ?");
|
|
13384
|
+
params.push(filter.is_template ? 1 : 0);
|
|
13385
|
+
}
|
|
13386
|
+
if (filter.search) {
|
|
13387
|
+
conditions.push("(name LIKE ? OR description LIKE ? OR content LIKE ?)");
|
|
13388
|
+
const q = `%${filter.search}%`;
|
|
13389
|
+
params.push(q, q, q);
|
|
13390
|
+
}
|
|
13391
|
+
if (filter.tags?.length) {
|
|
13392
|
+
conditions.push(`(${filter.tags.map(() => "tags LIKE ?").join(" AND ")})`);
|
|
13393
|
+
for (const tag of filter.tags)
|
|
13394
|
+
params.push(`%"${tag}"%`);
|
|
13395
|
+
}
|
|
13396
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
13397
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
13398
|
+
const total = d.query(`SELECT COUNT(*) AS total FROM configs ${where}`).get(...params)?.total ?? 0;
|
|
13399
|
+
const pageParams = [...params, normalized.limit, normalized.cursor];
|
|
13400
|
+
const rows = d.query(`SELECT id, name, slug, kind, category, agent, format, is_template, version, created_at, updated_at, synced_at
|
|
13401
|
+
FROM configs ${where} ORDER BY id LIMIT ? OFFSET ?`).all(...pageParams);
|
|
13402
|
+
return boundedReadPage(rows.map((row) => ({
|
|
13403
|
+
...row,
|
|
13404
|
+
kind: row.kind,
|
|
13405
|
+
category: row.category,
|
|
13406
|
+
agent: row.agent,
|
|
13407
|
+
format: row.format,
|
|
13408
|
+
is_template: Boolean(row.is_template)
|
|
13409
|
+
})), total, normalized);
|
|
13410
|
+
}
|
|
13411
|
+
function listConfigSummariesPage(filter = {}, options = {}, db) {
|
|
13412
|
+
const d = db || getDatabase();
|
|
13413
|
+
const conditions = [];
|
|
13414
|
+
const params = [];
|
|
13415
|
+
if (filter.category) {
|
|
13416
|
+
conditions.push("category = ?");
|
|
13417
|
+
params.push(filter.category);
|
|
13418
|
+
}
|
|
13419
|
+
if (filter.agent) {
|
|
13420
|
+
conditions.push("agent = ?");
|
|
13421
|
+
params.push(filter.agent);
|
|
13422
|
+
}
|
|
13423
|
+
if (filter.kind) {
|
|
13424
|
+
conditions.push("kind = ?");
|
|
13425
|
+
params.push(filter.kind);
|
|
13426
|
+
}
|
|
13427
|
+
if (filter.is_template !== undefined) {
|
|
13428
|
+
conditions.push("is_template = ?");
|
|
13429
|
+
params.push(filter.is_template ? 1 : 0);
|
|
13430
|
+
}
|
|
13431
|
+
if (filter.search) {
|
|
13432
|
+
conditions.push("(name LIKE ? OR description LIKE ? OR content LIKE ?)");
|
|
13433
|
+
const q = `%${filter.search}%`;
|
|
13434
|
+
params.push(q, q, q);
|
|
13435
|
+
}
|
|
13436
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
13437
|
+
conditions.push(`(${filter.tags.map(() => "tags LIKE ?").join(" AND ")})`);
|
|
13438
|
+
for (const tag of filter.tags)
|
|
13439
|
+
params.push(`%"${tag}"%`);
|
|
13440
|
+
}
|
|
13441
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
13442
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
13443
|
+
const total = d.query(`SELECT COUNT(*) AS total FROM configs ${where}`).get(...params)?.total ?? 0;
|
|
13444
|
+
const pageParams = [...params, normalized.limit, normalized.cursor];
|
|
13445
|
+
const rows = d.query(`SELECT id, name, slug, kind, category, agent, target_path, format,
|
|
13446
|
+
CASE WHEN json_valid(outputs) AND json_type(outputs) = 'array' THEN json_array_length(outputs) ELSE 0 END AS output_count, description, tags,
|
|
13447
|
+
is_template, version, updated_at
|
|
13448
|
+
FROM configs ${where} ORDER BY category, name, id LIMIT ? OFFSET ?`).all(...pageParams);
|
|
13449
|
+
return boundedReadPage(rows.map((row) => ({
|
|
13450
|
+
id: row.id,
|
|
13451
|
+
slug: row.slug,
|
|
13452
|
+
name: row.name,
|
|
13453
|
+
category: row.category,
|
|
13454
|
+
agent: row.agent,
|
|
13455
|
+
kind: row.kind,
|
|
13456
|
+
format: row.format,
|
|
13457
|
+
target_path: row.target_path,
|
|
13458
|
+
output_count: Number(row.output_count ?? 0),
|
|
13459
|
+
version: row.version,
|
|
13460
|
+
is_template: Boolean(row.is_template),
|
|
13461
|
+
updated_at: row.updated_at,
|
|
13462
|
+
description: row.description,
|
|
13463
|
+
tags: JSON.parse(row.tags || "[]")
|
|
13464
|
+
})), total, normalized);
|
|
13465
|
+
}
|
|
13329
13466
|
function updateConfig(idOrSlug, input, db) {
|
|
13330
13467
|
const d = db || getDatabase();
|
|
13331
13468
|
const existing = getConfig(idOrSlug, d);
|
|
@@ -13407,6 +13544,7 @@ var init_configs = __esm(() => {
|
|
|
13407
13544
|
init_types();
|
|
13408
13545
|
init_database();
|
|
13409
13546
|
init_snapshots();
|
|
13547
|
+
init_bounded_read();
|
|
13410
13548
|
});
|
|
13411
13549
|
|
|
13412
13550
|
// src/lib/template.ts
|
|
@@ -13847,7 +13985,10 @@ __export(exports_local, {
|
|
|
13847
13985
|
listSnapshots: () => listSnapshots,
|
|
13848
13986
|
listProfilesPage: () => listProfilesPage,
|
|
13849
13987
|
listMachines: () => listMachines,
|
|
13988
|
+
listConfigsPage: () => listConfigsPage,
|
|
13850
13989
|
listConfigs: () => listConfigs,
|
|
13990
|
+
listConfigSummariesPage: () => listConfigSummariesPage,
|
|
13991
|
+
listConfigIdentitiesPage: () => listConfigIdentitiesPage,
|
|
13851
13992
|
insertFeedback: () => insertFeedback,
|
|
13852
13993
|
getSnapshotByVersion: () => getSnapshotByVersion,
|
|
13853
13994
|
getSnapshot: () => getSnapshot,
|
|
@@ -13876,11 +14017,11 @@ var init_local = __esm(() => {
|
|
|
13876
14017
|
|
|
13877
14018
|
// src/data/config-store.ts
|
|
13878
14019
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
13879
|
-
function parseBoundedPagePayload(value, label) {
|
|
14020
|
+
function parseBoundedPagePayload(value, label, expected) {
|
|
13880
14021
|
const page = value;
|
|
13881
14022
|
const consumed = Number(page?.cursor) + (page?.items?.length ?? 0);
|
|
13882
14023
|
const complete = Boolean(page && Number.isSafeInteger(page.total) && consumed >= Number(page.total));
|
|
13883
|
-
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)) {
|
|
14024
|
+
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) || expected !== undefined && Number(page.cursor) !== expected.cursor) {
|
|
13884
14025
|
throw new Error(`${label} returned an invalid or truncated bounded-read envelope`);
|
|
13885
14026
|
}
|
|
13886
14027
|
return {
|
|
@@ -13892,7 +14033,7 @@ function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
|
|
|
13892
14033
|
if (value && typeof value === "object") {
|
|
13893
14034
|
const candidate = value;
|
|
13894
14035
|
if ("items" in candidate || "total" in candidate || "complete" in candidate || "truncated" in candidate || "next_cursor" in candidate) {
|
|
13895
|
-
return parseBoundedPagePayload(value, label);
|
|
14036
|
+
return parseBoundedPagePayload(value, label, normalizeBoundedReadOptions(options));
|
|
13896
14037
|
}
|
|
13897
14038
|
}
|
|
13898
14039
|
if (!Array.isArray(legacyItems)) {
|
|
@@ -13902,6 +14043,70 @@ function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
|
|
|
13902
14043
|
const page = boundedReadPage(legacyItems.slice(normalized.cursor, normalized.cursor + normalized.limit), legacyItems.length, normalized);
|
|
13903
14044
|
return { ...page, source_bounded: false };
|
|
13904
14045
|
}
|
|
14046
|
+
function projectBoundedPage(page, project) {
|
|
14047
|
+
return {
|
|
14048
|
+
items: page.items.map(project),
|
|
14049
|
+
total: page.total,
|
|
14050
|
+
limit: page.limit,
|
|
14051
|
+
cursor: page.cursor,
|
|
14052
|
+
next_cursor: page.next_cursor,
|
|
14053
|
+
has_more: page.has_more,
|
|
14054
|
+
complete: page.complete,
|
|
14055
|
+
truncated: page.truncated,
|
|
14056
|
+
source_bounded: page.source_bounded
|
|
14057
|
+
};
|
|
14058
|
+
}
|
|
14059
|
+
async function aggregateBoundedCollection(label, readPage, identity, options = {}) {
|
|
14060
|
+
let lastError = null;
|
|
14061
|
+
for (let attempt = 0;attempt < COLLECTION_READ_ATTEMPTS; attempt += 1) {
|
|
14062
|
+
try {
|
|
14063
|
+
const items = [];
|
|
14064
|
+
const seen = new Set;
|
|
14065
|
+
let expectedTotal = null;
|
|
14066
|
+
let cursor = 0;
|
|
14067
|
+
let previousIdentity = null;
|
|
14068
|
+
while (true) {
|
|
14069
|
+
const page = await readPage(cursor);
|
|
14070
|
+
if (page.cursor !== cursor) {
|
|
14071
|
+
throw new CollectionChangedWhilePagingError(label, `server returned cursor ${page.cursor} for requested cursor ${cursor}`);
|
|
14072
|
+
}
|
|
14073
|
+
if (expectedTotal === null)
|
|
14074
|
+
expectedTotal = page.total;
|
|
14075
|
+
else if (page.total !== expectedTotal) {
|
|
14076
|
+
throw new CollectionChangedWhilePagingError(label, `total changed from ${expectedTotal} to ${page.total}`);
|
|
14077
|
+
}
|
|
14078
|
+
for (const item of page.items) {
|
|
14079
|
+
const key = identity(item);
|
|
14080
|
+
if (!key)
|
|
14081
|
+
throw new CollectionChangedWhilePagingError(label, "an item had no stable identity");
|
|
14082
|
+
if (seen.has(key))
|
|
14083
|
+
throw new CollectionChangedWhilePagingError(label, `duplicate identity ${key}`);
|
|
14084
|
+
if (options.requireAscendingIdentity && page.source_bounded && previousIdentity !== null && key <= previousIdentity) {
|
|
14085
|
+
throw new CollectionChangedWhilePagingError(label, `identity order was not strictly increasing at ${key}`);
|
|
14086
|
+
}
|
|
14087
|
+
seen.add(key);
|
|
14088
|
+
previousIdentity = key;
|
|
14089
|
+
items.push(item);
|
|
14090
|
+
}
|
|
14091
|
+
if (page.complete) {
|
|
14092
|
+
if (items.length !== expectedTotal) {
|
|
14093
|
+
throw new CollectionChangedWhilePagingError(label, `received ${items.length} unique rows for total ${expectedTotal}`);
|
|
14094
|
+
}
|
|
14095
|
+
return items;
|
|
14096
|
+
}
|
|
14097
|
+
if (page.next_cursor === null || page.next_cursor <= cursor) {
|
|
14098
|
+
throw new CollectionChangedWhilePagingError(label, `cursor did not advance from ${cursor}`);
|
|
14099
|
+
}
|
|
14100
|
+
cursor = page.next_cursor;
|
|
14101
|
+
}
|
|
14102
|
+
} catch (error) {
|
|
14103
|
+
if (!(error instanceof CollectionChangedWhilePagingError))
|
|
14104
|
+
throw error;
|
|
14105
|
+
lastError = error;
|
|
14106
|
+
}
|
|
14107
|
+
}
|
|
14108
|
+
throw lastError ?? new CollectionChangedWhilePagingError(label, "read did not stabilize");
|
|
14109
|
+
}
|
|
13905
14110
|
function isLocalOptIn(env = process.env) {
|
|
13906
14111
|
return isInstructionsLocalOptIn(env);
|
|
13907
14112
|
}
|
|
@@ -13946,6 +14151,15 @@ class LocalConfigStore {
|
|
|
13946
14151
|
async listConfigs(filter) {
|
|
13947
14152
|
return (await localStoreModule()).listConfigs(filter, this.db);
|
|
13948
14153
|
}
|
|
14154
|
+
async listConfigsPage(filter = {}, options = {}) {
|
|
14155
|
+
return (await localStoreModule()).listConfigsPage(filter, options, this.db);
|
|
14156
|
+
}
|
|
14157
|
+
async listConfigIdentitiesPage(filter = {}, options = {}) {
|
|
14158
|
+
return (await localStoreModule()).listConfigIdentitiesPage(filter, options, this.db);
|
|
14159
|
+
}
|
|
14160
|
+
async listConfigSummariesPage(filter = {}, options = {}) {
|
|
14161
|
+
return (await localStoreModule()).listConfigSummariesPage(filter, options, this.db);
|
|
14162
|
+
}
|
|
13949
14163
|
async getConfig(idOrSlug) {
|
|
13950
14164
|
return (await localStoreModule()).getConfig(idOrSlug, this.db);
|
|
13951
14165
|
}
|
|
@@ -13980,15 +14194,7 @@ class LocalConfigStore {
|
|
|
13980
14194
|
return (await localStoreModule()).pruneSnapshots(configId, keep, this.db);
|
|
13981
14195
|
}
|
|
13982
14196
|
async listProfiles() {
|
|
13983
|
-
|
|
13984
|
-
let cursor = 0;
|
|
13985
|
-
while (true) {
|
|
13986
|
-
const page = await this.listProfilesPage({ limit: 100, cursor });
|
|
13987
|
-
profiles.push(...page.items);
|
|
13988
|
-
if (page.complete)
|
|
13989
|
-
return profiles;
|
|
13990
|
-
cursor = page.next_cursor;
|
|
13991
|
-
}
|
|
14197
|
+
return aggregateBoundedCollection("profile list", (cursor) => this.listProfilesPage({ limit: 100, cursor }), (profile) => profile.id);
|
|
13992
14198
|
}
|
|
13993
14199
|
async listProfilesPage(options = {}) {
|
|
13994
14200
|
return (await localStoreModule()).listProfilesPage(options, this.db);
|
|
@@ -13997,15 +14203,7 @@ class LocalConfigStore {
|
|
|
13997
14203
|
return (await localStoreModule()).getProfile(idOrSlug, this.db);
|
|
13998
14204
|
}
|
|
13999
14205
|
async getProfileConfigs(idOrSlug) {
|
|
14000
|
-
|
|
14001
|
-
let cursor = 0;
|
|
14002
|
-
while (true) {
|
|
14003
|
-
const page = await this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor });
|
|
14004
|
-
configs.push(...page.items);
|
|
14005
|
-
if (page.complete)
|
|
14006
|
-
return configs;
|
|
14007
|
-
cursor = page.next_cursor;
|
|
14008
|
-
}
|
|
14206
|
+
return aggregateBoundedCollection("profile membership", (cursor) => this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor }), (config) => config.id);
|
|
14009
14207
|
}
|
|
14010
14208
|
async getProfileConfigsPage(idOrSlug, options = {}) {
|
|
14011
14209
|
return (await localStoreModule()).getProfileConfigsPage(idOrSlug, options, this.db);
|
|
@@ -14065,6 +14263,43 @@ class LocalConfigStore {
|
|
|
14065
14263
|
(await localStoreModule()).resetLocalDatabase();
|
|
14066
14264
|
}
|
|
14067
14265
|
}
|
|
14266
|
+
function toConfigIdentity(value) {
|
|
14267
|
+
const record = value && typeof value === "object" ? value : {};
|
|
14268
|
+
return {
|
|
14269
|
+
id: String(record.id ?? ""),
|
|
14270
|
+
name: String(record.name ?? ""),
|
|
14271
|
+
slug: String(record.slug ?? ""),
|
|
14272
|
+
kind: String(record.kind ?? ""),
|
|
14273
|
+
category: String(record.category ?? ""),
|
|
14274
|
+
agent: String(record.agent ?? ""),
|
|
14275
|
+
format: String(record.format ?? ""),
|
|
14276
|
+
is_template: Boolean(record.is_template),
|
|
14277
|
+
version: Number(record.version ?? 0),
|
|
14278
|
+
created_at: String(record.created_at ?? ""),
|
|
14279
|
+
updated_at: String(record.updated_at ?? ""),
|
|
14280
|
+
synced_at: record.synced_at == null ? null : String(record.synced_at)
|
|
14281
|
+
};
|
|
14282
|
+
}
|
|
14283
|
+
function toConfigSummary(value) {
|
|
14284
|
+
const record = value && typeof value === "object" ? value : {};
|
|
14285
|
+
const outputs = Array.isArray(record.outputs) ? record.outputs : [];
|
|
14286
|
+
return {
|
|
14287
|
+
id: String(record.id ?? ""),
|
|
14288
|
+
slug: String(record.slug ?? ""),
|
|
14289
|
+
name: String(record.name ?? ""),
|
|
14290
|
+
category: String(record.category ?? ""),
|
|
14291
|
+
agent: String(record.agent ?? ""),
|
|
14292
|
+
kind: String(record.kind ?? ""),
|
|
14293
|
+
format: String(record.format ?? ""),
|
|
14294
|
+
target_path: record.target_path == null ? null : String(record.target_path),
|
|
14295
|
+
output_count: Number(record.output_count ?? outputs.length),
|
|
14296
|
+
version: Number(record.version ?? 0),
|
|
14297
|
+
is_template: Boolean(record.is_template),
|
|
14298
|
+
...record.updated_at !== undefined ? { updated_at: String(record.updated_at) } : {},
|
|
14299
|
+
...record.description !== undefined ? { description: record.description == null ? null : String(record.description) } : {},
|
|
14300
|
+
...Array.isArray(record.tags) ? { tags: record.tags.map(String) } : {}
|
|
14301
|
+
};
|
|
14302
|
+
}
|
|
14068
14303
|
|
|
14069
14304
|
class CloudConfigStore {
|
|
14070
14305
|
mode = "api";
|
|
@@ -14086,7 +14321,8 @@ class CloudConfigStore {
|
|
|
14086
14321
|
throw err;
|
|
14087
14322
|
}
|
|
14088
14323
|
}
|
|
14089
|
-
async
|
|
14324
|
+
async listConfigsPage(filter = {}, options = {}) {
|
|
14325
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
14090
14326
|
const params = new URLSearchParams;
|
|
14091
14327
|
if (filter.category)
|
|
14092
14328
|
params.set("category", filter.category);
|
|
@@ -14096,16 +14332,75 @@ class CloudConfigStore {
|
|
|
14096
14332
|
params.set("kind", filter.kind);
|
|
14097
14333
|
if (filter.search)
|
|
14098
14334
|
params.set("search", filter.search);
|
|
14099
|
-
const
|
|
14100
|
-
|
|
14101
|
-
|
|
14335
|
+
for (const tag of filter.tags ?? [])
|
|
14336
|
+
params.append("tag", tag);
|
|
14337
|
+
params.set("limit", String(normalized.limit));
|
|
14338
|
+
params.set("cursor", String(normalized.cursor));
|
|
14339
|
+
const { data } = await this.request("GET", `/configs?${params.toString()}`);
|
|
14340
|
+
return parseBoundedOrLegacyPage(data, data?.configs, normalized, "config list");
|
|
14341
|
+
}
|
|
14342
|
+
async listConfigIdentitiesPage(filter = {}, options = {}) {
|
|
14343
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
14344
|
+
const params = new URLSearchParams;
|
|
14345
|
+
if (filter.category)
|
|
14346
|
+
params.set("category", filter.category);
|
|
14347
|
+
if (filter.agent)
|
|
14348
|
+
params.set("agent", filter.agent);
|
|
14349
|
+
if (filter.kind)
|
|
14350
|
+
params.set("kind", filter.kind);
|
|
14351
|
+
if (filter.search)
|
|
14352
|
+
params.set("search", filter.search);
|
|
14353
|
+
for (const tag of filter.tags ?? [])
|
|
14354
|
+
params.append("tag", tag);
|
|
14355
|
+
params.set("view", "identity");
|
|
14356
|
+
params.set("limit", String(normalized.limit));
|
|
14357
|
+
params.set("cursor", String(normalized.cursor));
|
|
14358
|
+
const { data } = await this.request("GET", `/configs?${params.toString()}`);
|
|
14359
|
+
const page = parseBoundedOrLegacyPage(data, data?.configs, normalized, "config identity list");
|
|
14360
|
+
if (page.items.some((item) => {
|
|
14361
|
+
const record = item && typeof item === "object" ? item : {};
|
|
14362
|
+
return ["content", "target_path", "outputs", "description", "tags"].some((field) => Object.prototype.hasOwnProperty.call(record, field));
|
|
14363
|
+
})) {
|
|
14364
|
+
throw new Error("Instructions /v1 config identity projection is unavailable; deploy the identity endpoint before using compact lists");
|
|
14365
|
+
}
|
|
14366
|
+
return projectBoundedPage(page, toConfigIdentity);
|
|
14367
|
+
}
|
|
14368
|
+
async listConfigSummariesPage(filter = {}, options = {}) {
|
|
14369
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
14370
|
+
const params = new URLSearchParams;
|
|
14371
|
+
if (filter.category)
|
|
14372
|
+
params.set("category", filter.category);
|
|
14373
|
+
if (filter.agent)
|
|
14374
|
+
params.set("agent", filter.agent);
|
|
14375
|
+
if (filter.kind)
|
|
14376
|
+
params.set("kind", filter.kind);
|
|
14377
|
+
if (filter.search)
|
|
14378
|
+
params.set("search", filter.search);
|
|
14379
|
+
for (const tag of filter.tags ?? [])
|
|
14380
|
+
params.append("tag", tag);
|
|
14381
|
+
params.set("view", "summary");
|
|
14382
|
+
params.set("limit", String(normalized.limit));
|
|
14383
|
+
params.set("cursor", String(normalized.cursor));
|
|
14384
|
+
const { data } = await this.request("GET", `/configs?${params.toString()}`);
|
|
14385
|
+
const page = parseBoundedOrLegacyPage(data, data?.configs, normalized, "config summary list");
|
|
14386
|
+
if (page.items.some((item) => {
|
|
14387
|
+
const record = item && typeof item === "object" ? item : {};
|
|
14388
|
+
return Object.prototype.hasOwnProperty.call(record, "content") || Object.prototype.hasOwnProperty.call(record, "outputs");
|
|
14389
|
+
})) {
|
|
14390
|
+
throw new Error("Instructions /v1 config summary projection is unavailable; deploy the summary endpoint before using summary lists");
|
|
14391
|
+
}
|
|
14392
|
+
return projectBoundedPage(page, toConfigSummary);
|
|
14393
|
+
}
|
|
14394
|
+
async listConfigs(filter = {}) {
|
|
14395
|
+
const configs = await aggregateBoundedCollection("config list", (cursor) => this.listConfigsPage(filter, { limit: 100, cursor }), (config) => config.id, { requireAscendingIdentity: true });
|
|
14396
|
+
let filtered = configs;
|
|
14102
14397
|
if (filter.tags && filter.tags.length > 0) {
|
|
14103
|
-
|
|
14398
|
+
filtered = filtered.filter((config) => filter.tags.every((tag) => config.tags.includes(tag)));
|
|
14104
14399
|
}
|
|
14105
14400
|
if (filter.is_template !== undefined) {
|
|
14106
|
-
|
|
14401
|
+
filtered = filtered.filter((config) => config.is_template === filter.is_template);
|
|
14107
14402
|
}
|
|
14108
|
-
return
|
|
14403
|
+
return filtered;
|
|
14109
14404
|
}
|
|
14110
14405
|
async getConfig(idOrSlug) {
|
|
14111
14406
|
const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
@@ -14136,8 +14431,11 @@ class CloudConfigStore {
|
|
|
14136
14431
|
return data ?? { total: 0 };
|
|
14137
14432
|
}
|
|
14138
14433
|
async listSnapshots(configId) {
|
|
14139
|
-
|
|
14140
|
-
|
|
14434
|
+
return aggregateBoundedCollection("snapshot list", async (cursor) => {
|
|
14435
|
+
const normalized = normalizeBoundedReadOptions({ limit: 100, cursor });
|
|
14436
|
+
const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots?limit=${normalized.limit}&cursor=${normalized.cursor}`);
|
|
14437
|
+
return parseBoundedOrLegacyPage(data, data?.snapshots, normalized, "snapshot list");
|
|
14438
|
+
}, (snapshot) => snapshot.id);
|
|
14141
14439
|
}
|
|
14142
14440
|
async getSnapshot(id) {
|
|
14143
14441
|
const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
|
|
@@ -14160,15 +14458,7 @@ class CloudConfigStore {
|
|
|
14160
14458
|
return data?.pruned ?? 0;
|
|
14161
14459
|
}
|
|
14162
14460
|
async listProfiles() {
|
|
14163
|
-
|
|
14164
|
-
let cursor = 0;
|
|
14165
|
-
while (true) {
|
|
14166
|
-
const page = await this.listProfilesPage({ limit: 100, cursor });
|
|
14167
|
-
profiles.push(...page.items);
|
|
14168
|
-
if (page.complete)
|
|
14169
|
-
return profiles;
|
|
14170
|
-
cursor = page.next_cursor;
|
|
14171
|
-
}
|
|
14461
|
+
return aggregateBoundedCollection("profile list", (cursor) => this.listProfilesPage({ limit: 100, cursor }), (profile) => profile.id);
|
|
14172
14462
|
}
|
|
14173
14463
|
async listProfilesPage(options = {}) {
|
|
14174
14464
|
const normalized = normalizeBoundedReadOptions(options);
|
|
@@ -14192,15 +14482,7 @@ class CloudConfigStore {
|
|
|
14192
14482
|
return profile;
|
|
14193
14483
|
}
|
|
14194
14484
|
async getProfileConfigs(idOrSlug) {
|
|
14195
|
-
|
|
14196
|
-
let cursor = 0;
|
|
14197
|
-
while (true) {
|
|
14198
|
-
const page = await this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor });
|
|
14199
|
-
configs.push(...page.items);
|
|
14200
|
-
if (page.complete)
|
|
14201
|
-
return configs;
|
|
14202
|
-
cursor = page.next_cursor;
|
|
14203
|
-
}
|
|
14485
|
+
return aggregateBoundedCollection("profile membership", (cursor) => this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor }), (config) => config.id);
|
|
14204
14486
|
}
|
|
14205
14487
|
async getProfileConfigsPage(idOrSlug, options = {}) {
|
|
14206
14488
|
const normalized = normalizeBoundedReadOptions(options);
|
|
@@ -14214,20 +14496,26 @@ class CloudConfigStore {
|
|
|
14214
14496
|
return parseBoundedOrLegacyPage(data.configs, data.profile.configs, normalized, "profile membership");
|
|
14215
14497
|
}
|
|
14216
14498
|
async getProfileConfigBindings(idOrSlug) {
|
|
14217
|
-
|
|
14218
|
-
|
|
14219
|
-
const
|
|
14220
|
-
const
|
|
14221
|
-
|
|
14222
|
-
|
|
14223
|
-
|
|
14224
|
-
|
|
14225
|
-
|
|
14226
|
-
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14499
|
+
let routeMissing = false;
|
|
14500
|
+
const bindings = await aggregateBoundedCollection("profile config bindings", async (cursor) => {
|
|
14501
|
+
const normalized = normalizeBoundedReadOptions({ limit: 100, cursor });
|
|
14502
|
+
const { status, data } = await this.requestProfileRoute(idOrSlug, (profileId) => `/profiles/${encodeURIComponent(profileId)}/bindings?limit=${normalized.limit}&cursor=${normalized.cursor}`, (value) => Boolean(value && (Array.isArray(value.bindings) || Array.isArray(value.items))));
|
|
14503
|
+
if (status === 404) {
|
|
14504
|
+
routeMissing = true;
|
|
14505
|
+
return { ...boundedReadPage([], 0, normalized), source_bounded: false };
|
|
14506
|
+
}
|
|
14507
|
+
return parseBoundedOrLegacyPage(data, data?.bindings, normalized, "profile config bindings");
|
|
14508
|
+
}, (binding) => `${binding.profile_id}\x00${binding.config_id}`);
|
|
14509
|
+
if (!routeMissing)
|
|
14510
|
+
return bindings;
|
|
14511
|
+
const profile = await this.getProfile(idOrSlug);
|
|
14512
|
+
const configs = await this.getProfileConfigs(idOrSlug);
|
|
14513
|
+
return configs.map((config, sort_order) => ({
|
|
14514
|
+
profile_id: profile.id,
|
|
14515
|
+
config_id: config.id,
|
|
14516
|
+
sort_order,
|
|
14517
|
+
binding: legacyProfileConfigBinding()
|
|
14518
|
+
}));
|
|
14231
14519
|
}
|
|
14232
14520
|
async requestProfileRoute(idOrSlug, pathForId, isUsable) {
|
|
14233
14521
|
const attempted = new Set;
|
|
@@ -14285,14 +14573,20 @@ class CloudConfigStore {
|
|
|
14285
14573
|
await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
|
|
14286
14574
|
}
|
|
14287
14575
|
async getProfileAssetBindings(profileIdOrSlug) {
|
|
14288
|
-
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14576
|
+
let routeMissing = false;
|
|
14577
|
+
const assets = await aggregateBoundedCollection("profile asset bindings", async (cursor) => {
|
|
14578
|
+
const normalized = normalizeBoundedReadOptions({ limit: 100, cursor });
|
|
14579
|
+
const { status, data } = await this.requestProfileRoute(profileIdOrSlug, (profileId) => `/profiles/${encodeURIComponent(profileId)}/assets?limit=${normalized.limit}&cursor=${normalized.cursor}`, (value) => Boolean(value && (Array.isArray(value.assets) || Array.isArray(value.items))));
|
|
14580
|
+
if (status === 404) {
|
|
14581
|
+
routeMissing = true;
|
|
14582
|
+
return { ...boundedReadPage([], 0, normalized), source_bounded: false };
|
|
14583
|
+
}
|
|
14584
|
+
return parseBoundedOrLegacyPage(data, data?.assets, normalized, "profile asset bindings");
|
|
14585
|
+
}, (asset) => `${asset.profile_id}\x00${asset.binding.assetKey}`);
|
|
14586
|
+
if (!routeMissing)
|
|
14587
|
+
return assets;
|
|
14588
|
+
await this.getProfile(profileIdOrSlug);
|
|
14589
|
+
return [];
|
|
14296
14590
|
}
|
|
14297
14591
|
async addAssetToProfile(profileIdOrSlug, sourceConfigId, binding) {
|
|
14298
14592
|
const { data } = await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/assets`, { source_config_id: sourceConfigId, binding }, { idempotent: true });
|
|
@@ -14362,8 +14656,11 @@ class CloudConfigStore {
|
|
|
14362
14656
|
await this.request("POST", "/machines/applied", { hostname: hostname2 });
|
|
14363
14657
|
}
|
|
14364
14658
|
async listMachines() {
|
|
14365
|
-
|
|
14366
|
-
|
|
14659
|
+
return aggregateBoundedCollection("machine list", async (cursor) => {
|
|
14660
|
+
const normalized = normalizeBoundedReadOptions({ limit: 100, cursor });
|
|
14661
|
+
const { data } = await this.request("GET", `/machines?limit=${normalized.limit}&cursor=${normalized.cursor}`);
|
|
14662
|
+
return parseBoundedOrLegacyPage(data, data?.machines, normalized, "machine list");
|
|
14663
|
+
}, (machine) => machine.id, { requireAscendingIdentity: true });
|
|
14367
14664
|
}
|
|
14368
14665
|
async sendFeedback(input) {
|
|
14369
14666
|
await this.request("POST", "/feedback", {
|
|
@@ -14386,13 +14683,19 @@ function resolveConfigStore(env = process.env, options = {}) {
|
|
|
14386
14683
|
const { client } = resolveInstructionsStorageClient(env, options);
|
|
14387
14684
|
return new CloudConfigStore(client);
|
|
14388
14685
|
}
|
|
14389
|
-
var LOCAL_OPT_IN_ENV, localStoreModulePromise = null;
|
|
14686
|
+
var CollectionChangedWhilePagingError, COLLECTION_READ_ATTEMPTS = 2, LOCAL_OPT_IN_ENV, localStoreModulePromise = null;
|
|
14390
14687
|
var init_config_store = __esm(() => {
|
|
14391
14688
|
init_types();
|
|
14392
14689
|
init_bounded_read();
|
|
14393
14690
|
init_instruction_graph();
|
|
14394
14691
|
init_transport_resolver();
|
|
14395
14692
|
init_local_opt_in();
|
|
14693
|
+
CollectionChangedWhilePagingError = class CollectionChangedWhilePagingError extends Error {
|
|
14694
|
+
constructor(label, detail) {
|
|
14695
|
+
super(`${label} changed while paging: ${detail}`);
|
|
14696
|
+
this.name = "CollectionChangedWhilePagingError";
|
|
14697
|
+
}
|
|
14698
|
+
};
|
|
14396
14699
|
LOCAL_OPT_IN_ENV = INSTRUCTIONS_LOCAL_OPT_IN_ENV;
|
|
14397
14700
|
});
|
|
14398
14701
|
|
|
@@ -14415,7 +14718,7 @@ init_config_agents();
|
|
|
14415
14718
|
init_machine();
|
|
14416
14719
|
init_session_render();
|
|
14417
14720
|
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, realpathSync as realpathSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
14418
|
-
import { basename as basename5, dirname as dirname6, join as join12, resolve as resolve9 } from "path";
|
|
14721
|
+
import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute6, join as join12, relative as relative4, resolve as resolve9, sep as sep2 } from "path";
|
|
14419
14722
|
import { homedir as homedir7 } from "os";
|
|
14420
14723
|
|
|
14421
14724
|
// src/lib/session-render-ownership.ts
|
|
@@ -14504,6 +14807,17 @@ function expandPath(p) {
|
|
|
14504
14807
|
}
|
|
14505
14808
|
return resolve9(p);
|
|
14506
14809
|
}
|
|
14810
|
+
function compactPathForConfigHome(p) {
|
|
14811
|
+
const absolute = resolve9(p);
|
|
14812
|
+
const configHome = resolve9(getConfigHome());
|
|
14813
|
+
const conventionalHome = resolve9(process.env["HOME"] || homedir7());
|
|
14814
|
+
if (configHome !== conventionalHome)
|
|
14815
|
+
return absolute;
|
|
14816
|
+
const rel = relative4(configHome, absolute);
|
|
14817
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute6(rel))
|
|
14818
|
+
return absolute;
|
|
14819
|
+
return `~/${rel}`;
|
|
14820
|
+
}
|
|
14507
14821
|
function normalizeTargetPath(p) {
|
|
14508
14822
|
const expanded = expandPath(p);
|
|
14509
14823
|
try {
|
|
@@ -14957,7 +15271,7 @@ import {
|
|
|
14957
15271
|
writeFileSync as writeFileSync3
|
|
14958
15272
|
} from "fs";
|
|
14959
15273
|
import { homedir as homedir8 } from "os";
|
|
14960
|
-
import { dirname as dirname8, join as join14, parse as parse4, relative as
|
|
15274
|
+
import { dirname as dirname8, join as join14, parse as parse4, relative as relative5, resolve as resolve10 } from "path";
|
|
14961
15275
|
var INBOX_CONVERSATIONS_MINIMUM_VERSION = "0.5.28";
|
|
14962
15276
|
var INBOX_SKILL_MARKERS = [
|
|
14963
15277
|
[".claude", "skills", "inbox", "SKILL.md"],
|
|
@@ -14981,7 +15295,7 @@ function findSymlinkedAncestor(path) {
|
|
|
14981
15295
|
const normalized = resolve10(path);
|
|
14982
15296
|
const parsed = parse4(normalized);
|
|
14983
15297
|
let current = parsed.root;
|
|
14984
|
-
const rel =
|
|
15298
|
+
const rel = relative5(parsed.root, normalized);
|
|
14985
15299
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
14986
15300
|
current = join14(current, segment);
|
|
14987
15301
|
if (!existsSync11(current))
|
|
@@ -15000,16 +15314,7 @@ function assertNoSymlinkAncestors2(path) {
|
|
|
15000
15314
|
function packagedInboxSkillPath(explicitPath) {
|
|
15001
15315
|
if (explicitPath)
|
|
15002
15316
|
return explicitPath;
|
|
15003
|
-
|
|
15004
|
-
join14(import.meta.dir, "..", "..", "assets", "skills", "inbox", "SKILL.md"),
|
|
15005
|
-
join14(import.meta.dir, "..", "assets", "skills", "inbox", "SKILL.md"),
|
|
15006
|
-
join14(process.cwd(), "assets", "skills", "inbox", "SKILL.md")
|
|
15007
|
-
];
|
|
15008
|
-
const found = candidates.find((candidate) => existsSync11(candidate));
|
|
15009
|
-
if (!found) {
|
|
15010
|
-
throw new Error(`packaged inbox skill contract is missing (checked ${candidates.length} package-relative locations)`);
|
|
15011
|
-
}
|
|
15012
|
-
return found;
|
|
15317
|
+
throw new Error("Bundled skill contracts are retired; use the Skills CLI to manage private skills");
|
|
15013
15318
|
}
|
|
15014
15319
|
function readCanonicalSkill(explicitPath) {
|
|
15015
15320
|
const assetPath = packagedInboxSkillPath(explicitPath);
|
|
@@ -16028,7 +16333,7 @@ import {
|
|
|
16028
16333
|
readdirSync as readdirSync3,
|
|
16029
16334
|
statSync as statSync5
|
|
16030
16335
|
} from "fs";
|
|
16031
|
-
import { dirname as dirname10, isAbsolute as
|
|
16336
|
+
import { dirname as dirname10, isAbsolute as isAbsolute7, join as join17, parse as parse5, relative as relative6, resolve as resolve11 } from "path";
|
|
16032
16337
|
|
|
16033
16338
|
class SessionApplyError extends Error {
|
|
16034
16339
|
constructor(message) {
|
|
@@ -16166,7 +16471,7 @@ function ensureSessionTargetHome(targetHome) {
|
|
|
16166
16471
|
}
|
|
16167
16472
|
function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
16168
16473
|
const safeTargetHome = assertSafeTargetHome(targetHome);
|
|
16169
|
-
const resolvedManifestPath = manifestPath ? resolveManifestRelativePath(
|
|
16474
|
+
const resolvedManifestPath = manifestPath ? resolveManifestRelativePath(relative6(safeTargetHome, resolve11(manifestPath)), safeTargetHome) : resolve11(safeTargetHome, ".hasna", "session-render-manifest.json");
|
|
16170
16475
|
const checkedAt = new Date().toISOString();
|
|
16171
16476
|
const previousManifest = readPreviousManifest(resolvedManifestPath);
|
|
16172
16477
|
if (!previousManifest) {
|
|
@@ -16218,11 +16523,11 @@ function restoreSessionRenderSnapshot(snapshotPath, options = {}) {
|
|
|
16218
16523
|
const targetHome = assertSafeTargetHome(snapshot.targetHome);
|
|
16219
16524
|
const resolvedSnapshotPath = resolve11(snapshotPath);
|
|
16220
16525
|
const snapshotDir = getSessionRenderSnapshotDir(targetHome);
|
|
16221
|
-
const snapshotDirRelative =
|
|
16222
|
-
const insideSnapshotDir = snapshotDirRelative !== ".." && !snapshotDirRelative.startsWith("../") && !
|
|
16526
|
+
const snapshotDirRelative = relative6(snapshotDir, resolvedSnapshotPath);
|
|
16527
|
+
const insideSnapshotDir = snapshotDirRelative !== ".." && !snapshotDirRelative.startsWith("../") && !isAbsolute7(snapshotDirRelative);
|
|
16223
16528
|
if (!insideSnapshotDir) {
|
|
16224
|
-
const snapshotRelativePath =
|
|
16225
|
-
if (snapshotRelativePath === "" || snapshotRelativePath === ".." || snapshotRelativePath.startsWith("../") ||
|
|
16529
|
+
const snapshotRelativePath = relative6(targetHome, resolvedSnapshotPath);
|
|
16530
|
+
if (snapshotRelativePath === "" || snapshotRelativePath === ".." || snapshotRelativePath.startsWith("../") || isAbsolute7(snapshotRelativePath)) {
|
|
16226
16531
|
throw new SessionApplyError("Session snapshot must be stored inside its session-render snapshot location.");
|
|
16227
16532
|
}
|
|
16228
16533
|
}
|
|
@@ -16421,7 +16726,7 @@ function readSessionRenderSnapshot(snapshotPath) {
|
|
|
16421
16726
|
function reconstructPreRollbackLegacyV1Snapshot(snapshot, previousFiles, previousManifestFiles, targetHome, snapshotPath) {
|
|
16422
16727
|
assertNoNewerSessionSnapshot(snapshotPath, snapshot.createdAt, targetHome);
|
|
16423
16728
|
const manifestPath = resolve11(snapshot.manifestPath);
|
|
16424
|
-
const manifestRelativePath =
|
|
16729
|
+
const manifestRelativePath = relative6(targetHome, manifestPath).replaceAll("\\", "/");
|
|
16425
16730
|
resolveSnapshotFilePath(manifestRelativePath, snapshot.manifestPath, targetHome);
|
|
16426
16731
|
const manifestSha256 = currentSessionFileHash(manifestPath, targetHome);
|
|
16427
16732
|
if (manifestSha256 === null) {
|
|
@@ -16754,8 +17059,8 @@ function isPreviouslyManaged(file, previousSha256, previousHashes, previousManif
|
|
|
16754
17059
|
}
|
|
16755
17060
|
function resolvePlannedFilePath(plan, file, targetHome) {
|
|
16756
17061
|
const target = resolve11(targetHome, ...file.relativePath.split("/"));
|
|
16757
|
-
const rel =
|
|
16758
|
-
if (rel === "" || rel === ".." || rel.startsWith("../") ||
|
|
17062
|
+
const rel = relative6(targetHome, target);
|
|
17063
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute7(rel)) {
|
|
16759
17064
|
throw new SessionApplyError(`Session file escapes target home: ${file.relativePath}`);
|
|
16760
17065
|
}
|
|
16761
17066
|
if (resolve11(file.path) !== target) {
|
|
@@ -16766,8 +17071,8 @@ function resolvePlannedFilePath(plan, file, targetHome) {
|
|
|
16766
17071
|
}
|
|
16767
17072
|
function resolveManifestRelativePath(relativePath, targetHome) {
|
|
16768
17073
|
const target = resolve11(targetHome, ...relativePath.split(/[\\/]+/));
|
|
16769
|
-
const rel =
|
|
16770
|
-
if (rel === "" || rel === ".." || rel.startsWith("../") ||
|
|
17074
|
+
const rel = relative6(targetHome, target);
|
|
17075
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute7(rel)) {
|
|
16771
17076
|
throw new SessionApplyError(`Session manifest file escapes target home: ${relativePath}`);
|
|
16772
17077
|
}
|
|
16773
17078
|
assertNoSymlinkSegments2(targetHome, target);
|
|
@@ -16811,7 +17116,7 @@ function applyPlannedFile(plan, file, targetHome, resultsByPath, coordination, a
|
|
|
16811
17116
|
function assertExpectedSessionFileHash(path, targetHome, expectedHash) {
|
|
16812
17117
|
const actualHash = currentSessionFileHash(path, targetHome);
|
|
16813
17118
|
if (actualHash !== expectedHash) {
|
|
16814
|
-
throw new SessionApplyError(`Session apply path changed after planning: ${
|
|
17119
|
+
throw new SessionApplyError(`Session apply path changed after planning: ${relative6(targetHome, path)}`);
|
|
16815
17120
|
}
|
|
16816
17121
|
}
|
|
16817
17122
|
function currentSessionFileHash(path, targetHome) {
|
|
@@ -16896,7 +17201,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
16896
17201
|
};
|
|
16897
17202
|
}
|
|
16898
17203
|
function assertSafeTargetHome(targetHome) {
|
|
16899
|
-
if (!
|
|
17204
|
+
if (!isAbsolute7(targetHome))
|
|
16900
17205
|
throw new SessionApplyError(`Session target home must be absolute: ${targetHome}`);
|
|
16901
17206
|
const normalized = resolve11(targetHome);
|
|
16902
17207
|
if (normalized === parse5(normalized).root) {
|
|
@@ -16910,7 +17215,7 @@ function assertSafeTargetHome(targetHome) {
|
|
|
16910
17215
|
}
|
|
16911
17216
|
function assertNoSymlinkSegments2(root, target) {
|
|
16912
17217
|
assertNoSymlinkAncestors3(root);
|
|
16913
|
-
const rel =
|
|
17218
|
+
const rel = relative6(root, target);
|
|
16914
17219
|
let current = root;
|
|
16915
17220
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
16916
17221
|
current = join17(current, segment);
|
|
@@ -16923,7 +17228,7 @@ function assertNoSymlinkAncestors3(path) {
|
|
|
16923
17228
|
const normalized = resolve11(path);
|
|
16924
17229
|
const parsed = parse5(normalized);
|
|
16925
17230
|
let current = parsed.root;
|
|
16926
|
-
const rel =
|
|
17231
|
+
const rel = relative6(parsed.root, normalized);
|
|
16927
17232
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
16928
17233
|
current = join17(current, segment);
|
|
16929
17234
|
if (!existsSync15(current))
|
|
@@ -17268,8 +17573,7 @@ init_transforms();
|
|
|
17268
17573
|
// src/lib/sync-dir.ts
|
|
17269
17574
|
init_config_store();
|
|
17270
17575
|
import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync15, statSync as statSync6 } from "fs";
|
|
17271
|
-
import { join as join18, relative as
|
|
17272
|
-
import { homedir as homedir10 } from "os";
|
|
17576
|
+
import { join as join18, relative as relative7 } from "path";
|
|
17273
17577
|
init_redact();
|
|
17274
17578
|
var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
|
|
17275
17579
|
function shouldSkip(p) {
|
|
@@ -17282,7 +17586,6 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
17282
17586
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
17283
17587
|
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync4(absDir).map((f) => join18(absDir, f)).filter((f) => statSync6(f).isFile());
|
|
17284
17588
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
17285
|
-
const home = homedir10();
|
|
17286
17589
|
const allConfigs = await store.listConfigs();
|
|
17287
17590
|
for (const file of files) {
|
|
17288
17591
|
if (shouldSkip(file)) {
|
|
@@ -17295,12 +17598,12 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
17295
17598
|
result.skipped.push(file + " (too large)");
|
|
17296
17599
|
continue;
|
|
17297
17600
|
}
|
|
17298
|
-
const targetPath = file
|
|
17601
|
+
const targetPath = compactPathForConfigHome(file);
|
|
17299
17602
|
const redacted = redactContent(content, redactFormatForTarget(targetPath, detectFormat(file)));
|
|
17300
17603
|
const existing = allConfigs.find((c) => c.target_path === targetPath);
|
|
17301
17604
|
if (!existing) {
|
|
17302
17605
|
if (!opts.dryRun)
|
|
17303
|
-
await store.createConfig({ name:
|
|
17606
|
+
await store.createConfig({ name: relative7(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content: redacted.content });
|
|
17304
17607
|
result.added++;
|
|
17305
17608
|
} else if (existing.content !== redacted.content) {
|
|
17306
17609
|
if (!opts.dryRun)
|
|
@@ -17317,9 +17620,8 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
17317
17620
|
}
|
|
17318
17621
|
async function syncToDir(dir, opts = {}) {
|
|
17319
17622
|
const store = opts.store ?? resolveConfigStore();
|
|
17320
|
-
const home = homedir10();
|
|
17321
17623
|
const absDir = expandPath(dir);
|
|
17322
|
-
const normalized = dir.startsWith("~/") ? dir : absDir
|
|
17624
|
+
const normalized = dir.startsWith("~/") ? dir : compactPathForConfigHome(absDir);
|
|
17323
17625
|
const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
17324
17626
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
17325
17627
|
for (const config of configs) {
|
|
@@ -17869,112 +18171,680 @@ function detectFormat(filePath) {
|
|
|
17869
18171
|
return "text";
|
|
17870
18172
|
}
|
|
17871
18173
|
// src/lib/export.ts
|
|
18174
|
+
init_types();
|
|
17872
18175
|
init_config_store();
|
|
17873
|
-
import {
|
|
17874
|
-
import {
|
|
18176
|
+
import { createHash as createHash11 } from "crypto";
|
|
18177
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
18178
|
+
import { dirname as dirname11, join as join20, resolve as resolve12 } from "path";
|
|
17875
18179
|
import { tmpdir } from "os";
|
|
17876
|
-
|
|
18180
|
+
var ARCHIVE_EXCLUSIONS = [
|
|
18181
|
+
{
|
|
18182
|
+
entity: "api_keys",
|
|
18183
|
+
classification: "security_state",
|
|
18184
|
+
reason: "API credentials are security state and must be provisioned independently, never copied in a domain archive."
|
|
18185
|
+
},
|
|
18186
|
+
{
|
|
18187
|
+
entity: "idempotency_receipts",
|
|
18188
|
+
classification: "transport_state",
|
|
18189
|
+
reason: "Request replay receipts are transport state and are not portable business-domain data."
|
|
18190
|
+
},
|
|
18191
|
+
{
|
|
18192
|
+
entity: "feedback",
|
|
18193
|
+
classification: "out_of_domain",
|
|
18194
|
+
reason: "Feedback is product telemetry, not part of the Instructions configuration domain."
|
|
18195
|
+
}
|
|
18196
|
+
];
|
|
18197
|
+
function compareText(left, right) {
|
|
18198
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
18199
|
+
}
|
|
18200
|
+
function sha25611(value) {
|
|
18201
|
+
return createHash11("sha256").update(value).digest("hex");
|
|
18202
|
+
}
|
|
18203
|
+
function canonicalize(value) {
|
|
18204
|
+
if (Array.isArray(value))
|
|
18205
|
+
return value.map(canonicalize);
|
|
18206
|
+
if (value && typeof value === "object") {
|
|
18207
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => compareText(left, right)).map(([key, entry]) => [key, canonicalize(entry)]));
|
|
18208
|
+
}
|
|
18209
|
+
return value;
|
|
18210
|
+
}
|
|
18211
|
+
function canonicalDomainJson(value) {
|
|
18212
|
+
return JSON.stringify(canonicalize(value));
|
|
18213
|
+
}
|
|
18214
|
+
function logicalAssetLocator(locator, idToSlug) {
|
|
18215
|
+
const match = /^config:\/\/([^@]+)@(\d+)$/.exec(locator);
|
|
18216
|
+
if (!match)
|
|
18217
|
+
return locator;
|
|
18218
|
+
let configId;
|
|
18219
|
+
try {
|
|
18220
|
+
configId = decodeURIComponent(match[1]);
|
|
18221
|
+
} catch {
|
|
18222
|
+
return locator;
|
|
18223
|
+
}
|
|
18224
|
+
const slug2 = idToSlug.get(configId);
|
|
18225
|
+
return slug2 ? `config-slug://${encodeURIComponent(slug2)}@${match[2]}` : locator;
|
|
18226
|
+
}
|
|
18227
|
+
function logicalCollections(domain, options) {
|
|
18228
|
+
const configs = [...domain.configs].sort((left, right) => compareText(left.slug, right.slug));
|
|
18229
|
+
const snapshots = domain.config_snapshots.map((snapshot) => ({
|
|
18230
|
+
config_slug: snapshot.config_slug,
|
|
18231
|
+
version: snapshot.version,
|
|
18232
|
+
content: snapshot.content,
|
|
18233
|
+
...options.includeOperationalTimestamps ? { created_at: snapshot.created_at } : {}
|
|
18234
|
+
})).sort((left, right) => compareText(left.config_slug, right.config_slug) || left.version - right.version || compareText(left.content, right.content) || compareText("created_at" in left ? left.created_at ?? "" : "", "created_at" in right ? right.created_at ?? "" : ""));
|
|
18235
|
+
const profiles = [...domain.profiles].sort((left, right) => compareText(left.slug, right.slug));
|
|
18236
|
+
const configBindings = [...domain.profile_config_bindings].sort((left, right) => compareText(left.profile_slug, right.profile_slug) || left.sort_order - right.sort_order || compareText(left.config_slug, right.config_slug));
|
|
18237
|
+
const assetBindings = [...domain.profile_asset_bindings].sort((left, right) => compareText(left.profile_slug, right.profile_slug) || left.sort_order - right.sort_order || compareText(left.binding.assetKey, right.binding.assetKey));
|
|
18238
|
+
const machines = [...domain.machines].sort((left, right) => compareText(left.hostname, right.hostname));
|
|
18239
|
+
const idToSlug = new Map(configs.map((config) => [config.id, config.slug]));
|
|
18240
|
+
const configOrder = new Map;
|
|
18241
|
+
const assetOrder = new Map;
|
|
18242
|
+
return {
|
|
18243
|
+
configs: configs.map((config) => ({
|
|
18244
|
+
slug: config.slug,
|
|
18245
|
+
name: config.name,
|
|
18246
|
+
kind: config.kind,
|
|
18247
|
+
category: config.category,
|
|
18248
|
+
agent: config.agent,
|
|
18249
|
+
target_path: config.target_path,
|
|
18250
|
+
outputs: config.outputs,
|
|
18251
|
+
format: config.format,
|
|
18252
|
+
content: config.content,
|
|
18253
|
+
description: config.description,
|
|
18254
|
+
tags: [...config.tags].sort(compareText),
|
|
18255
|
+
is_template: config.is_template,
|
|
18256
|
+
version: config.version,
|
|
18257
|
+
...options.includeOperationalTimestamps ? {
|
|
18258
|
+
created_at: config.created_at,
|
|
18259
|
+
updated_at: config.updated_at,
|
|
18260
|
+
synced_at: config.synced_at
|
|
18261
|
+
} : {}
|
|
18262
|
+
})),
|
|
18263
|
+
config_snapshots: snapshots,
|
|
18264
|
+
profiles: profiles.map((profile) => ({
|
|
18265
|
+
slug: profile.slug,
|
|
18266
|
+
name: profile.name,
|
|
18267
|
+
description: profile.description,
|
|
18268
|
+
selectors: profile.selectors,
|
|
18269
|
+
variables: profile.variables,
|
|
18270
|
+
...options.includeOperationalTimestamps ? {
|
|
18271
|
+
created_at: profile.created_at,
|
|
18272
|
+
updated_at: profile.updated_at
|
|
18273
|
+
} : {}
|
|
18274
|
+
})),
|
|
18275
|
+
profile_config_bindings: configBindings.map((row) => {
|
|
18276
|
+
const order = configOrder.get(row.profile_slug) ?? 0;
|
|
18277
|
+
configOrder.set(row.profile_slug, order + 1);
|
|
18278
|
+
return {
|
|
18279
|
+
profile_slug: row.profile_slug,
|
|
18280
|
+
config_slug: row.config_slug,
|
|
18281
|
+
order,
|
|
18282
|
+
binding: row.binding
|
|
18283
|
+
};
|
|
18284
|
+
}),
|
|
18285
|
+
profile_asset_bindings: assetBindings.map((row) => {
|
|
18286
|
+
const order = assetOrder.get(row.profile_slug) ?? 0;
|
|
18287
|
+
assetOrder.set(row.profile_slug, order + 1);
|
|
18288
|
+
return {
|
|
18289
|
+
profile_slug: row.profile_slug,
|
|
18290
|
+
source_config_slug: row.source_config_slug,
|
|
18291
|
+
order,
|
|
18292
|
+
binding: {
|
|
18293
|
+
...row.binding,
|
|
18294
|
+
source: {
|
|
18295
|
+
...row.binding.source,
|
|
18296
|
+
locator: logicalAssetLocator(row.binding.source.locator, idToSlug)
|
|
18297
|
+
}
|
|
18298
|
+
}
|
|
18299
|
+
};
|
|
18300
|
+
}),
|
|
18301
|
+
machines: machines.map((machine) => ({
|
|
18302
|
+
hostname: machine.hostname,
|
|
18303
|
+
os: machine.os,
|
|
18304
|
+
arch: machine.arch,
|
|
18305
|
+
...options.includeOperationalTimestamps ? {
|
|
18306
|
+
created_at: machine.created_at,
|
|
18307
|
+
last_applied_at: machine.last_applied_at
|
|
18308
|
+
} : {
|
|
18309
|
+
applied: machine.last_applied_at !== null
|
|
18310
|
+
}
|
|
18311
|
+
}))
|
|
18312
|
+
};
|
|
18313
|
+
}
|
|
18314
|
+
function computeIntegrity(domain, options) {
|
|
18315
|
+
const collections = logicalCollections(domain, options);
|
|
18316
|
+
const counts = {
|
|
18317
|
+
configs: collections.configs.length,
|
|
18318
|
+
config_snapshots: collections.config_snapshots.length,
|
|
18319
|
+
profiles: collections.profiles.length,
|
|
18320
|
+
profile_config_bindings: collections.profile_config_bindings.length,
|
|
18321
|
+
profile_asset_bindings: collections.profile_asset_bindings.length,
|
|
18322
|
+
machines: collections.machines.length
|
|
18323
|
+
};
|
|
18324
|
+
const hashes = {
|
|
18325
|
+
configs: sha25611(canonicalDomainJson(collections.configs)),
|
|
18326
|
+
config_snapshots: sha25611(canonicalDomainJson(collections.config_snapshots)),
|
|
18327
|
+
profiles: sha25611(canonicalDomainJson(collections.profiles)),
|
|
18328
|
+
profile_config_bindings: sha25611(canonicalDomainJson(collections.profile_config_bindings)),
|
|
18329
|
+
profile_asset_bindings: sha25611(canonicalDomainJson(collections.profile_asset_bindings)),
|
|
18330
|
+
machines: sha25611(canonicalDomainJson(collections.machines))
|
|
18331
|
+
};
|
|
18332
|
+
return {
|
|
18333
|
+
algorithm: "sha256",
|
|
18334
|
+
canonicalization: options.canonicalization,
|
|
18335
|
+
counts,
|
|
18336
|
+
hashes,
|
|
18337
|
+
domain_sha256: sha25611(canonicalDomainJson({ counts, hashes }))
|
|
18338
|
+
};
|
|
18339
|
+
}
|
|
18340
|
+
function computeDomainIntegrity(domain) {
|
|
18341
|
+
return computeIntegrity(domain, {
|
|
18342
|
+
includeOperationalTimestamps: true,
|
|
18343
|
+
canonicalization: "hasna.instructions.logical-json/v1"
|
|
18344
|
+
});
|
|
18345
|
+
}
|
|
18346
|
+
function computeRestorableDomainIntegrity(domain) {
|
|
18347
|
+
return computeIntegrity(domain, {
|
|
18348
|
+
includeOperationalTimestamps: false,
|
|
18349
|
+
canonicalization: "hasna.instructions.restorable-logical-json/v1"
|
|
18350
|
+
});
|
|
18351
|
+
}
|
|
18352
|
+
function assertArray(value, label) {
|
|
18353
|
+
if (!Array.isArray(value))
|
|
18354
|
+
throw new Error(`Invalid v2 archive: ${label} must be an array`);
|
|
18355
|
+
}
|
|
18356
|
+
function assertUnique(values, label) {
|
|
18357
|
+
const seen = new Set;
|
|
18358
|
+
for (const value of values) {
|
|
18359
|
+
if (!value || seen.has(value))
|
|
18360
|
+
throw new Error(`Invalid v2 archive: duplicate or empty ${label} ${JSON.stringify(value)}`);
|
|
18361
|
+
seen.add(value);
|
|
18362
|
+
}
|
|
18363
|
+
}
|
|
18364
|
+
function assertTimestamp(value, label, nullable = false) {
|
|
18365
|
+
if (nullable && value === null)
|
|
18366
|
+
return;
|
|
18367
|
+
if (typeof value !== "string" || value.length === 0 || Number.isNaN(Date.parse(value))) {
|
|
18368
|
+
throw new Error(`Invalid v2 archive: ${label} must be ${nullable ? "null or " : ""}an ISO timestamp`);
|
|
18369
|
+
}
|
|
18370
|
+
}
|
|
18371
|
+
function validateInstructionsDomainArchive(value) {
|
|
18372
|
+
if (!value || typeof value !== "object")
|
|
18373
|
+
throw new Error("Invalid v2 archive: domain payload must be an object");
|
|
18374
|
+
const domain = value;
|
|
18375
|
+
if (domain.schema !== INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA)
|
|
18376
|
+
throw new Error("Invalid v2 archive: unsupported domain schema");
|
|
18377
|
+
assertArray(domain.configs, "configs");
|
|
18378
|
+
assertArray(domain.config_snapshots, "config_snapshots");
|
|
18379
|
+
assertArray(domain.profiles, "profiles");
|
|
18380
|
+
assertArray(domain.profile_config_bindings, "profile_config_bindings");
|
|
18381
|
+
assertArray(domain.profile_asset_bindings, "profile_asset_bindings");
|
|
18382
|
+
assertArray(domain.machines, "machines");
|
|
18383
|
+
const typed = domain;
|
|
18384
|
+
assertUnique(typed.configs.map((row) => row.slug), "config slug");
|
|
18385
|
+
assertUnique(typed.profiles.map((row) => row.slug), "profile slug");
|
|
18386
|
+
assertUnique(typed.machines.map((row) => row.hostname), "machine hostname");
|
|
18387
|
+
assertUnique(typed.config_snapshots.map((row) => row.id), "config snapshot id");
|
|
18388
|
+
assertUnique(typed.profile_config_bindings.map((row) => `${row.profile_slug}:${row.config_slug}`), "profile config binding");
|
|
18389
|
+
assertUnique(typed.profile_asset_bindings.map((row) => `${row.profile_slug}:${row.binding.assetKey}`), "profile asset binding");
|
|
18390
|
+
const configsBySlug = new Map(typed.configs.map((row) => [row.slug, row]));
|
|
18391
|
+
const configSlugs = new Set(configsBySlug.keys());
|
|
18392
|
+
const profileSlugs = new Set(typed.profiles.map((row) => row.slug));
|
|
18393
|
+
for (const config of typed.configs) {
|
|
18394
|
+
assertTimestamp(config.created_at, `config ${config.slug} created_at`);
|
|
18395
|
+
assertTimestamp(config.updated_at, `config ${config.slug} updated_at`);
|
|
18396
|
+
assertTimestamp(config.synced_at, `config ${config.slug} synced_at`, true);
|
|
18397
|
+
if (!Number.isSafeInteger(config.version) || config.version < 1)
|
|
18398
|
+
throw new Error(`Invalid v2 archive: config ${config.slug} has invalid version`);
|
|
18399
|
+
}
|
|
18400
|
+
for (const row of typed.config_snapshots) {
|
|
18401
|
+
const config = configsBySlug.get(row.config_slug);
|
|
18402
|
+
if (!config) {
|
|
18403
|
+
throw new Error(`Invalid v2 archive: snapshot references missing config ${row.config_slug}`);
|
|
18404
|
+
}
|
|
18405
|
+
if (!Number.isSafeInteger(row.version) || row.version < 1 || row.version > config.version) {
|
|
18406
|
+
throw new Error(`Invalid v2 archive: config ${row.config_slug} has an invalid snapshot version`);
|
|
18407
|
+
}
|
|
18408
|
+
assertTimestamp(row.created_at, `snapshot ${row.config_slug}@${row.version} created_at`);
|
|
18409
|
+
}
|
|
18410
|
+
for (const profile of typed.profiles) {
|
|
18411
|
+
assertTimestamp(profile.created_at, `profile ${profile.slug} created_at`);
|
|
18412
|
+
assertTimestamp(profile.updated_at, `profile ${profile.slug} updated_at`);
|
|
18413
|
+
}
|
|
18414
|
+
for (const row of typed.profile_config_bindings) {
|
|
18415
|
+
if (!profileSlugs.has(row.profile_slug) || !configSlugs.has(row.config_slug)) {
|
|
18416
|
+
throw new Error(`Invalid v2 archive: profile config binding has a missing logical reference`);
|
|
18417
|
+
}
|
|
18418
|
+
}
|
|
18419
|
+
for (const row of typed.profile_asset_bindings) {
|
|
18420
|
+
if (!profileSlugs.has(row.profile_slug) || !configSlugs.has(row.source_config_slug)) {
|
|
18421
|
+
throw new Error(`Invalid v2 archive: profile asset binding has a missing logical reference`);
|
|
18422
|
+
}
|
|
18423
|
+
}
|
|
18424
|
+
for (const machine of typed.machines) {
|
|
18425
|
+
assertTimestamp(machine.created_at, `machine ${machine.hostname} created_at`);
|
|
18426
|
+
assertTimestamp(machine.last_applied_at, `machine ${machine.hostname} last_applied_at`, true);
|
|
18427
|
+
}
|
|
18428
|
+
return typed;
|
|
18429
|
+
}
|
|
18430
|
+
function matchesFilter(config, filter) {
|
|
18431
|
+
if (!filter)
|
|
18432
|
+
return true;
|
|
18433
|
+
if (filter.category && config.category !== filter.category)
|
|
18434
|
+
return false;
|
|
18435
|
+
if (filter.agent && config.agent !== filter.agent)
|
|
18436
|
+
return false;
|
|
18437
|
+
if (filter.kind && config.kind !== filter.kind)
|
|
18438
|
+
return false;
|
|
18439
|
+
if (filter.is_template !== undefined && config.is_template !== filter.is_template)
|
|
18440
|
+
return false;
|
|
18441
|
+
if (filter.tags?.length && !filter.tags.every((tag) => config.tags.includes(tag)))
|
|
18442
|
+
return false;
|
|
18443
|
+
if (filter.search) {
|
|
18444
|
+
const needle = filter.search.toLocaleLowerCase();
|
|
18445
|
+
if (![config.name, config.description ?? "", config.content].some((value) => value.toLocaleLowerCase().includes(needle)))
|
|
18446
|
+
return false;
|
|
18447
|
+
}
|
|
18448
|
+
return true;
|
|
18449
|
+
}
|
|
18450
|
+
async function collectInstructionsDomain(opts = {}) {
|
|
17877
18451
|
const store = opts.store ?? resolveConfigStore();
|
|
17878
|
-
const
|
|
18452
|
+
const selectedProfile = opts.profileId ? await store.getProfile(opts.profileId) : null;
|
|
18453
|
+
const listedConfigs = selectedProfile ? await store.getProfileConfigs(selectedProfile.id) : await store.listConfigs(opts.filter);
|
|
18454
|
+
const configs = listedConfigs.filter((config) => matchesFilter(config, selectedProfile ? opts.filter : undefined));
|
|
18455
|
+
configs.sort((left, right) => compareText(left.slug, right.slug));
|
|
18456
|
+
const selectedIds = new Set(configs.map((config) => config.id));
|
|
18457
|
+
const profiles = (selectedProfile ? [selectedProfile] : await store.listProfiles()).sort((left, right) => compareText(left.slug, right.slug));
|
|
18458
|
+
const profileById = new Map(profiles.map((profile) => [profile.id, profile]));
|
|
18459
|
+
const configById = new Map(configs.map((config) => [config.id, config]));
|
|
18460
|
+
const configSnapshots = [];
|
|
18461
|
+
for (const config of configs) {
|
|
18462
|
+
for (const snapshot of await store.listSnapshots(config.id)) {
|
|
18463
|
+
configSnapshots.push({
|
|
18464
|
+
id: snapshot.id,
|
|
18465
|
+
config_slug: config.slug,
|
|
18466
|
+
content: snapshot.content,
|
|
18467
|
+
version: snapshot.version,
|
|
18468
|
+
created_at: snapshot.created_at
|
|
18469
|
+
});
|
|
18470
|
+
}
|
|
18471
|
+
}
|
|
18472
|
+
configSnapshots.sort((left, right) => compareText(left.config_slug, right.config_slug) || left.version - right.version || compareText(left.id, right.id));
|
|
18473
|
+
const profileConfigBindings = (await Promise.all(profiles.map(async (profile) => (await store.getProfileConfigBindings(profile.id)).filter((row) => selectedIds.has(row.config_id)).map((row) => ({
|
|
18474
|
+
profile_slug: profile.slug,
|
|
18475
|
+
config_slug: configById.get(row.config_id).slug,
|
|
18476
|
+
sort_order: row.sort_order,
|
|
18477
|
+
binding: row.binding
|
|
18478
|
+
}))))).flat().sort((left, right) => compareText(left.profile_slug, right.profile_slug) || left.sort_order - right.sort_order || compareText(left.config_slug, right.config_slug));
|
|
18479
|
+
const profileAssetBindings = (await Promise.all(profiles.map(async (profile) => (await store.getProfileAssetBindings(profile.id)).filter((row) => selectedIds.has(row.source_config_id)).map((row) => ({
|
|
18480
|
+
profile_slug: profile.slug,
|
|
18481
|
+
source_config_slug: configById.get(row.source_config_id).slug,
|
|
18482
|
+
sort_order: row.sort_order,
|
|
18483
|
+
binding: row.binding
|
|
18484
|
+
}))))).flat().sort((left, right) => compareText(left.profile_slug, right.profile_slug) || left.sort_order - right.sort_order || compareText(left.binding.assetKey, right.binding.assetKey));
|
|
18485
|
+
const archivedProfiles = profiles.filter((profile) => profileById.has(profile.id));
|
|
18486
|
+
const machines = (await store.listMachines()).sort((left, right) => compareText(left.hostname, right.hostname));
|
|
18487
|
+
return {
|
|
18488
|
+
schema: INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
|
|
18489
|
+
configs,
|
|
18490
|
+
config_snapshots: configSnapshots,
|
|
18491
|
+
profiles: archivedProfiles,
|
|
18492
|
+
profile_config_bindings: profileConfigBindings,
|
|
18493
|
+
profile_asset_bindings: profileAssetBindings,
|
|
18494
|
+
machines
|
|
18495
|
+
};
|
|
18496
|
+
}
|
|
18497
|
+
async function exportConfigs(outputPath, opts = {}) {
|
|
18498
|
+
const domain = validateInstructionsDomainArchive(await collectInstructionsDomain(opts));
|
|
18499
|
+
const integrity = computeDomainIntegrity(domain);
|
|
18500
|
+
const payload = `${JSON.stringify(domain, null, 2)}
|
|
18501
|
+
`;
|
|
18502
|
+
const manifest = {
|
|
18503
|
+
schema: INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
|
|
18504
|
+
version: "2.0.0",
|
|
18505
|
+
exported_at: new Date().toISOString(),
|
|
18506
|
+
payload: {
|
|
18507
|
+
path: "domain.json",
|
|
18508
|
+
sha256: sha25611(payload),
|
|
18509
|
+
size_bytes: Buffer.byteLength(payload)
|
|
18510
|
+
},
|
|
18511
|
+
integrity,
|
|
18512
|
+
exclusions: ARCHIVE_EXCLUSIONS
|
|
18513
|
+
};
|
|
17879
18514
|
const absOutput = resolve12(outputPath);
|
|
17880
|
-
|
|
17881
|
-
const
|
|
18515
|
+
mkdirSync8(dirname11(absOutput), { recursive: true });
|
|
18516
|
+
const stagingDir = mkdtempSync(join20(tmpdir(), "instructions-domain-export-"));
|
|
17882
18517
|
try {
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
17886
|
-
|
|
17887
|
-
configs: configs.map(({ content: _content, ...meta }) => meta)
|
|
17888
|
-
};
|
|
17889
|
-
writeFileSync6(join20(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
17890
|
-
for (const config of configs) {
|
|
17891
|
-
const fileName = `${config.slug}.${config.format === "text" ? "txt" : config.format}`;
|
|
17892
|
-
writeFileSync6(join20(contentsDir, fileName), config.content, "utf-8");
|
|
17893
|
-
}
|
|
17894
|
-
const proc = Bun.spawn(["tar", "czf", absOutput, "-C", tmpDir, "."], {
|
|
18518
|
+
writeFileSync6(join20(stagingDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
18519
|
+
`, "utf-8");
|
|
18520
|
+
writeFileSync6(join20(stagingDir, "domain.json"), payload, "utf-8");
|
|
18521
|
+
const proc = Bun.spawn(["tar", "czf", absOutput, "-C", stagingDir, "manifest.json", "domain.json"], {
|
|
17895
18522
|
stdout: "pipe",
|
|
17896
18523
|
stderr: "pipe"
|
|
17897
18524
|
});
|
|
17898
18525
|
const exitCode = await proc.exited;
|
|
17899
18526
|
if (exitCode !== 0) {
|
|
17900
18527
|
const stderr = await new Response(proc.stderr).text();
|
|
17901
|
-
throw new Error(`tar failed: ${stderr}`);
|
|
18528
|
+
throw new Error(`tar failed: ${stderr.trim()}`);
|
|
17902
18529
|
}
|
|
17903
|
-
return { path: absOutput, count: configs.length };
|
|
18530
|
+
return { path: absOutput, count: domain.configs.length, counts: integrity.counts, integrity };
|
|
17904
18531
|
} finally {
|
|
17905
|
-
if (existsSync18(
|
|
17906
|
-
rmSync4(
|
|
17907
|
-
}
|
|
18532
|
+
if (existsSync18(stagingDir))
|
|
18533
|
+
rmSync4(stagingDir, { recursive: true, force: true });
|
|
17908
18534
|
}
|
|
17909
18535
|
}
|
|
17910
18536
|
// src/lib/import.ts
|
|
18537
|
+
init_types();
|
|
17911
18538
|
init_config_store();
|
|
17912
|
-
import {
|
|
17913
|
-
import {
|
|
17914
|
-
|
|
17915
|
-
|
|
17916
|
-
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
|
|
17920
|
-
|
|
18539
|
+
import { createHash as createHash12 } from "crypto";
|
|
18540
|
+
import { resolve as resolve13 } from "path";
|
|
18541
|
+
function compareText2(left, right) {
|
|
18542
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
18543
|
+
}
|
|
18544
|
+
function emptyMutationCounts() {
|
|
18545
|
+
return { created: 0, updated: 0, skipped: 0 };
|
|
18546
|
+
}
|
|
18547
|
+
function emptyResult() {
|
|
18548
|
+
return {
|
|
18549
|
+
created: 0,
|
|
18550
|
+
updated: 0,
|
|
18551
|
+
skipped: 0,
|
|
18552
|
+
counts: {
|
|
18553
|
+
configs: emptyMutationCounts(),
|
|
18554
|
+
config_snapshots: { created: 0, skipped: 0 },
|
|
18555
|
+
profiles: emptyMutationCounts(),
|
|
18556
|
+
profile_config_bindings: emptyMutationCounts(),
|
|
18557
|
+
profile_asset_bindings: emptyMutationCounts(),
|
|
18558
|
+
machines: emptyMutationCounts()
|
|
18559
|
+
},
|
|
18560
|
+
errors: [],
|
|
18561
|
+
integrity: null
|
|
18562
|
+
};
|
|
18563
|
+
}
|
|
18564
|
+
function sha25612(value) {
|
|
18565
|
+
return createHash12("sha256").update(value).digest("hex");
|
|
18566
|
+
}
|
|
18567
|
+
function normalizeMemberName(name) {
|
|
18568
|
+
let normalized = name;
|
|
18569
|
+
while (normalized.startsWith("./"))
|
|
18570
|
+
normalized = normalized.slice(2);
|
|
18571
|
+
if (normalized.startsWith("/") || normalized.includes("\x00") || normalized.split("/").some((segment) => segment === "..")) {
|
|
18572
|
+
throw new Error(`Invalid archive member path: ${JSON.stringify(name)}`);
|
|
18573
|
+
}
|
|
18574
|
+
return normalized;
|
|
18575
|
+
}
|
|
18576
|
+
async function openArchive(path) {
|
|
18577
|
+
const list = Bun.spawn(["tar", "tzf", path], { stdout: "pipe", stderr: "pipe" });
|
|
18578
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
18579
|
+
list.exited,
|
|
18580
|
+
new Response(list.stdout).text(),
|
|
18581
|
+
new Response(list.stderr).text()
|
|
18582
|
+
]);
|
|
18583
|
+
if (exitCode !== 0)
|
|
18584
|
+
throw new Error(`tar listing failed: ${stderr.trim()}`);
|
|
18585
|
+
const members = new Map;
|
|
18586
|
+
for (const raw of stdout.split(`
|
|
18587
|
+
`).filter(Boolean)) {
|
|
18588
|
+
const normalized = normalizeMemberName(raw);
|
|
18589
|
+
if (normalized.length === 0)
|
|
18590
|
+
continue;
|
|
18591
|
+
if (members.has(normalized))
|
|
18592
|
+
throw new Error(`Invalid archive: duplicate member ${normalized}`);
|
|
18593
|
+
members.set(normalized, raw);
|
|
18594
|
+
}
|
|
18595
|
+
return {
|
|
18596
|
+
members,
|
|
18597
|
+
async read(member) {
|
|
18598
|
+
const actual = members.get(member);
|
|
18599
|
+
if (!actual)
|
|
18600
|
+
throw new Error(`Invalid archive: missing ${member}`);
|
|
18601
|
+
const proc = Bun.spawn(["tar", "xOzf", path, "--", actual], { stdout: "pipe", stderr: "pipe" });
|
|
18602
|
+
const [code, content, memberError] = await Promise.all([
|
|
18603
|
+
proc.exited,
|
|
18604
|
+
new Response(proc.stdout).text(),
|
|
18605
|
+
new Response(proc.stderr).text()
|
|
18606
|
+
]);
|
|
18607
|
+
if (code !== 0)
|
|
18608
|
+
throw new Error(`tar read failed for ${member}: ${memberError.trim()}`);
|
|
18609
|
+
return content;
|
|
18610
|
+
}
|
|
18611
|
+
};
|
|
18612
|
+
}
|
|
18613
|
+
function isNotFound(error) {
|
|
18614
|
+
return Boolean(error && typeof error === "object" && (error.name === "ConfigNotFoundError" || error.name === "ProfileNotFoundError" || error.status === 404));
|
|
18615
|
+
}
|
|
18616
|
+
async function findConfig(store, slug2) {
|
|
17921
18617
|
try {
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
17925
|
-
|
|
18618
|
+
return await store.getConfig(slug2);
|
|
18619
|
+
} catch (error) {
|
|
18620
|
+
if (isNotFound(error))
|
|
18621
|
+
return null;
|
|
18622
|
+
throw error;
|
|
18623
|
+
}
|
|
18624
|
+
}
|
|
18625
|
+
function verifyCounts(actual, expected) {
|
|
18626
|
+
if (canonicalDomainJson(actual) !== canonicalDomainJson(expected)) {
|
|
18627
|
+
throw new Error("Archive integrity failure: manifest counts do not match the domain payload");
|
|
18628
|
+
}
|
|
18629
|
+
}
|
|
18630
|
+
async function readV2(reader, manifest) {
|
|
18631
|
+
if (manifest.schema !== INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA || manifest.version !== "2.0.0" || manifest.payload?.path !== "domain.json" || manifest.integrity?.algorithm !== "sha256" || manifest.integrity?.canonicalization !== "hasna.instructions.logical-json/v1") {
|
|
18632
|
+
throw new Error("Invalid v2 archive manifest");
|
|
18633
|
+
}
|
|
18634
|
+
const payload = await reader.read("domain.json");
|
|
18635
|
+
if (Buffer.byteLength(payload) !== manifest.payload.size_bytes || sha25612(payload) !== manifest.payload.sha256) {
|
|
18636
|
+
throw new Error("Archive integrity failure: domain payload hash or size does not match the manifest");
|
|
18637
|
+
}
|
|
18638
|
+
const domain = validateInstructionsDomainArchive(JSON.parse(payload));
|
|
18639
|
+
const actual = computeDomainIntegrity(domain);
|
|
18640
|
+
verifyCounts(actual.counts, manifest.integrity.counts);
|
|
18641
|
+
if (canonicalDomainJson(actual) !== canonicalDomainJson(manifest.integrity)) {
|
|
18642
|
+
throw new Error("Archive integrity failure: logical domain hashes do not match the manifest");
|
|
18643
|
+
}
|
|
18644
|
+
return domain;
|
|
18645
|
+
}
|
|
18646
|
+
function configInput(config, content) {
|
|
18647
|
+
return {
|
|
18648
|
+
name: config.name,
|
|
18649
|
+
kind: config.kind,
|
|
18650
|
+
category: config.category,
|
|
18651
|
+
agent: config.agent,
|
|
18652
|
+
target_path: config.target_path,
|
|
18653
|
+
outputs: config.outputs,
|
|
18654
|
+
format: config.format,
|
|
18655
|
+
content,
|
|
18656
|
+
description: config.description ?? undefined,
|
|
18657
|
+
tags: config.tags,
|
|
18658
|
+
is_template: config.is_template
|
|
18659
|
+
};
|
|
18660
|
+
}
|
|
18661
|
+
async function restoreNewConfig(store, archived, domain) {
|
|
18662
|
+
const snapshots = domain.config_snapshots.filter((row) => row.config_slug === archived.slug).sort((left, right) => left.version - right.version || compareText2(left.content, right.content) || compareText2(left.id, right.id));
|
|
18663
|
+
let restored = await store.createConfig(configInput(archived, archived.content));
|
|
18664
|
+
for (let version = 2;version <= archived.version; version++) {
|
|
18665
|
+
restored = await store.updateConfig(restored.id, {
|
|
18666
|
+
content: archived.content,
|
|
18667
|
+
...version === archived.version ? {
|
|
18668
|
+
name: archived.name,
|
|
18669
|
+
kind: archived.kind,
|
|
18670
|
+
category: archived.category,
|
|
18671
|
+
agent: archived.agent,
|
|
18672
|
+
target_path: archived.target_path,
|
|
18673
|
+
outputs: archived.outputs,
|
|
18674
|
+
format: archived.format,
|
|
18675
|
+
description: archived.description,
|
|
18676
|
+
tags: archived.tags,
|
|
18677
|
+
is_template: archived.is_template,
|
|
18678
|
+
synced_at: archived.synced_at
|
|
18679
|
+
} : {}
|
|
17926
18680
|
});
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
|
|
17930
|
-
|
|
18681
|
+
}
|
|
18682
|
+
await store.pruneSnapshots(restored.id, 0);
|
|
18683
|
+
for (const snapshot of snapshots) {
|
|
18684
|
+
await store.createSnapshot(restored.id, snapshot.content, snapshot.version);
|
|
18685
|
+
}
|
|
18686
|
+
const restoredSnapshots = (await store.listSnapshots(restored.id)).map(({ version, content: snapshotContent }) => ({ version, content: snapshotContent })).sort((left, right) => left.version - right.version || compareText2(left.content, right.content));
|
|
18687
|
+
const expectedSnapshots = snapshots.map(({ version, content: snapshotContent }) => ({ version, content: snapshotContent })).sort((left, right) => left.version - right.version || compareText2(left.content, right.content));
|
|
18688
|
+
if (restored.version !== archived.version || restored.content !== archived.content || canonicalDomainJson(restoredSnapshots) !== canonicalDomainJson(expectedSnapshots)) {
|
|
18689
|
+
throw new Error(`restored config or snapshot history did not match archive for ${archived.slug}`);
|
|
18690
|
+
}
|
|
18691
|
+
return restored;
|
|
18692
|
+
}
|
|
18693
|
+
function rewriteAssetLocator(binding, archivedSourceId, restoredSourceId) {
|
|
18694
|
+
const match = /^config:\/\/([^@]+)@(\d+)$/.exec(binding.source.locator);
|
|
18695
|
+
if (!match)
|
|
18696
|
+
return binding;
|
|
18697
|
+
let locatorId;
|
|
18698
|
+
try {
|
|
18699
|
+
locatorId = decodeURIComponent(match[1]);
|
|
18700
|
+
} catch {
|
|
18701
|
+
return binding;
|
|
18702
|
+
}
|
|
18703
|
+
if (locatorId !== archivedSourceId)
|
|
18704
|
+
return binding;
|
|
18705
|
+
return {
|
|
18706
|
+
...binding,
|
|
18707
|
+
source: {
|
|
18708
|
+
...binding.source,
|
|
18709
|
+
locator: `config://${encodeURIComponent(restoredSourceId)}@${match[2]}`
|
|
17931
18710
|
}
|
|
17932
|
-
|
|
17933
|
-
|
|
17934
|
-
|
|
17935
|
-
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
|
|
17948
|
-
|
|
17949
|
-
|
|
17950
|
-
|
|
17951
|
-
|
|
17952
|
-
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
|
|
17957
|
-
|
|
17958
|
-
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
17963
|
-
|
|
17964
|
-
|
|
17965
|
-
|
|
17966
|
-
|
|
17967
|
-
|
|
17968
|
-
|
|
17969
|
-
|
|
18711
|
+
};
|
|
18712
|
+
}
|
|
18713
|
+
async function assertEmptyV2Destination(store) {
|
|
18714
|
+
const [configs, profiles, machines] = await Promise.all([
|
|
18715
|
+
store.listConfigs(),
|
|
18716
|
+
store.listProfiles(),
|
|
18717
|
+
store.listMachines()
|
|
18718
|
+
]);
|
|
18719
|
+
if (configs.length > 0 || profiles.length > 0 || machines.length > 0) {
|
|
18720
|
+
throw new Error(`Instructions domain archive v2 recovery requires an empty destination ` + `(found configs=${configs.length}, profiles=${profiles.length}, machines=${machines.length})`);
|
|
18721
|
+
}
|
|
18722
|
+
}
|
|
18723
|
+
async function importV2(domain, manifest, store, conflict) {
|
|
18724
|
+
if (conflict !== "skip") {
|
|
18725
|
+
throw new Error(`Instructions domain archive v2 does not support ${conflict}; restore into an empty destination`);
|
|
18726
|
+
}
|
|
18727
|
+
await assertEmptyV2Destination(store);
|
|
18728
|
+
const result = emptyResult();
|
|
18729
|
+
result.integrity = manifest.integrity;
|
|
18730
|
+
const configMap = new Map;
|
|
18731
|
+
for (const archived of domain.configs) {
|
|
18732
|
+
const restored = await restoreNewConfig(store, archived, domain);
|
|
18733
|
+
configMap.set(archived.slug, restored);
|
|
18734
|
+
result.counts.configs.created++;
|
|
18735
|
+
result.created++;
|
|
18736
|
+
result.counts.config_snapshots.created += domain.config_snapshots.filter((row) => row.config_slug === archived.slug).length;
|
|
18737
|
+
}
|
|
18738
|
+
const profileMap = new Map;
|
|
18739
|
+
for (const archived of domain.profiles) {
|
|
18740
|
+
const created = await store.createProfile({
|
|
18741
|
+
name: archived.name,
|
|
18742
|
+
description: archived.description ?? undefined,
|
|
18743
|
+
selectors: archived.selectors,
|
|
18744
|
+
variables: archived.variables
|
|
18745
|
+
});
|
|
18746
|
+
profileMap.set(archived.slug, created);
|
|
18747
|
+
result.counts.profiles.created++;
|
|
18748
|
+
}
|
|
18749
|
+
for (const archived of domain.profiles) {
|
|
18750
|
+
const restoredProfile = profileMap.get(archived.slug);
|
|
18751
|
+
if (!restoredProfile)
|
|
18752
|
+
throw new Error(`post-create profile mapping missing for ${archived.slug}`);
|
|
18753
|
+
const archivedMembership = domain.profile_config_bindings.filter((row) => row.profile_slug === archived.slug).sort((left, right) => left.sort_order - right.sort_order || compareText2(left.config_slug, right.config_slug));
|
|
18754
|
+
const archivedAssets = domain.profile_asset_bindings.filter((row) => row.profile_slug === archived.slug).sort((left, right) => left.sort_order - right.sort_order || compareText2(left.binding.assetKey, right.binding.assetKey));
|
|
18755
|
+
for (const row of archivedMembership) {
|
|
18756
|
+
const config = configMap.get(row.config_slug);
|
|
18757
|
+
if (!config)
|
|
18758
|
+
throw new Error(`missing restored config ${row.config_slug}`);
|
|
18759
|
+
await store.addConfigToProfile(restoredProfile.id, config.id);
|
|
18760
|
+
await store.setProfileConfigBinding(restoredProfile.id, config.id, row.binding);
|
|
18761
|
+
result.counts.profile_config_bindings.created++;
|
|
18762
|
+
}
|
|
18763
|
+
for (const row of archivedAssets) {
|
|
18764
|
+
const source = configMap.get(row.source_config_slug);
|
|
18765
|
+
const archivedSource = domain.configs.find((config) => config.slug === row.source_config_slug);
|
|
18766
|
+
if (!source || !archivedSource)
|
|
18767
|
+
throw new Error(`missing restored asset source ${row.source_config_slug}`);
|
|
18768
|
+
await store.addAssetToProfile(restoredProfile.id, source.id, rewriteAssetLocator(row.binding, archivedSource.id, source.id));
|
|
18769
|
+
result.counts.profile_asset_bindings.created++;
|
|
18770
|
+
}
|
|
18771
|
+
}
|
|
18772
|
+
for (const archived of domain.machines) {
|
|
18773
|
+
const restored = await store.registerMachine(archived.hostname, archived.os ?? undefined, archived.arch ?? undefined);
|
|
18774
|
+
if (archived.last_applied_at !== null)
|
|
18775
|
+
await store.updateMachineApplied(restored.hostname);
|
|
18776
|
+
result.counts.machines.created++;
|
|
18777
|
+
}
|
|
18778
|
+
const restoredDomain = await collectInstructionsDomain({ store });
|
|
18779
|
+
const expected = computeRestorableDomainIntegrity(domain);
|
|
18780
|
+
const actual = computeRestorableDomainIntegrity(restoredDomain);
|
|
18781
|
+
if (canonicalDomainJson(actual) !== canonicalDomainJson(expected)) {
|
|
18782
|
+
throw new Error(`Post-restore logical integrity verification failed: expected ${expected.domain_sha256}, got ${actual.domain_sha256}`);
|
|
18783
|
+
}
|
|
18784
|
+
return result;
|
|
18785
|
+
}
|
|
18786
|
+
async function importV1(reader, manifest, store, conflict) {
|
|
18787
|
+
const result = emptyResult();
|
|
18788
|
+
if (!Array.isArray(manifest.configs))
|
|
18789
|
+
throw new Error("Invalid v1 bundle: configs must be an array");
|
|
18790
|
+
for (const meta of manifest.configs) {
|
|
18791
|
+
const ext = meta.format === "text" ? "txt" : meta.format;
|
|
18792
|
+
const contentPath = `contents/${meta.slug}.${ext}`;
|
|
18793
|
+
const content = reader.members.has(contentPath) ? await reader.read(contentPath) : "";
|
|
18794
|
+
const existing = await findConfig(store, meta.slug);
|
|
18795
|
+
if (existing) {
|
|
18796
|
+
if (conflict === "skip") {
|
|
18797
|
+
result.skipped++;
|
|
18798
|
+
result.counts.configs.skipped++;
|
|
18799
|
+
} else {
|
|
18800
|
+
await store.updateConfig(existing.id, {
|
|
18801
|
+
name: meta.name,
|
|
18802
|
+
kind: meta.kind,
|
|
18803
|
+
category: meta.category,
|
|
18804
|
+
agent: meta.agent,
|
|
18805
|
+
target_path: meta.target_path,
|
|
18806
|
+
outputs: meta.outputs,
|
|
18807
|
+
format: meta.format,
|
|
18808
|
+
content,
|
|
18809
|
+
description: meta.description,
|
|
18810
|
+
tags: meta.tags,
|
|
18811
|
+
is_template: meta.is_template,
|
|
18812
|
+
synced_at: meta.synced_at
|
|
18813
|
+
});
|
|
18814
|
+
result.updated++;
|
|
18815
|
+
result.counts.configs.updated++;
|
|
17970
18816
|
}
|
|
18817
|
+
} else {
|
|
18818
|
+
await store.createConfig(configInput({ ...meta, content }, content));
|
|
18819
|
+
result.created++;
|
|
18820
|
+
result.counts.configs.created++;
|
|
17971
18821
|
}
|
|
17972
|
-
return result;
|
|
17973
|
-
} finally {
|
|
17974
|
-
if (existsSync19(tmpDir)) {
|
|
17975
|
-
rmSync5(tmpDir, { recursive: true, force: true });
|
|
17976
|
-
}
|
|
17977
18822
|
}
|
|
18823
|
+
return result;
|
|
18824
|
+
}
|
|
18825
|
+
async function importConfigs(bundlePath, opts = {}) {
|
|
18826
|
+
const store = opts.store ?? resolveConfigStore();
|
|
18827
|
+
const conflict = opts.conflict ?? "skip";
|
|
18828
|
+
const reader = await openArchive(resolve13(bundlePath));
|
|
18829
|
+
const manifestText = await reader.read("manifest.json");
|
|
18830
|
+
let manifest;
|
|
18831
|
+
try {
|
|
18832
|
+
manifest = JSON.parse(manifestText);
|
|
18833
|
+
} catch {
|
|
18834
|
+
throw new Error("Invalid bundle: manifest.json is not valid JSON");
|
|
18835
|
+
}
|
|
18836
|
+
if (!manifest || typeof manifest !== "object" || typeof manifest.version !== "string") {
|
|
18837
|
+
throw new Error("Invalid bundle: manifest version is missing");
|
|
18838
|
+
}
|
|
18839
|
+
if (manifest.version === "2.0.0") {
|
|
18840
|
+
const v2 = manifest;
|
|
18841
|
+
const domain = await readV2(reader, v2);
|
|
18842
|
+
return importV2(domain, v2, store, conflict);
|
|
18843
|
+
}
|
|
18844
|
+
if (manifest.version.startsWith("1.")) {
|
|
18845
|
+
return importV1(reader, manifest, store, conflict);
|
|
18846
|
+
}
|
|
18847
|
+
throw new Error(`Unsupported Instructions archive version: ${manifest.version}`);
|
|
17978
18848
|
}
|
|
17979
18849
|
|
|
17980
18850
|
// src/index.ts
|
|
@@ -17983,9 +18853,9 @@ init_redact();
|
|
|
17983
18853
|
|
|
17984
18854
|
// src/lib/package-manager-guard.ts
|
|
17985
18855
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
17986
|
-
import { existsSync as
|
|
17987
|
-
import { homedir as
|
|
17988
|
-
import { basename as basename7, dirname as
|
|
18856
|
+
import { existsSync as existsSync19, lstatSync as lstatSync7, readdirSync as readdirSync6, readFileSync as readFileSync17 } from "fs";
|
|
18857
|
+
import { homedir as homedir10 } from "os";
|
|
18858
|
+
import { basename as basename7, dirname as dirname12, isAbsolute as isAbsolute8, join as join21, relative as relative8, resolve as resolve14 } from "path";
|
|
17989
18859
|
var SKIP_DIRS = new Set([
|
|
17990
18860
|
".git",
|
|
17991
18861
|
"node_modules",
|
|
@@ -18027,7 +18897,7 @@ function scanPackageManagerSecrets(options = {}) {
|
|
|
18027
18897
|
const findings = [];
|
|
18028
18898
|
let scannedFiles = 0;
|
|
18029
18899
|
for (const root of roots) {
|
|
18030
|
-
if (!
|
|
18900
|
+
if (!existsSync19(root))
|
|
18031
18901
|
continue;
|
|
18032
18902
|
const stat = lstatSync7(root);
|
|
18033
18903
|
if (stat.isFile()) {
|
|
@@ -18037,14 +18907,14 @@ function scanPackageManagerSecrets(options = {}) {
|
|
|
18037
18907
|
if (text === null)
|
|
18038
18908
|
continue;
|
|
18039
18909
|
scannedFiles++;
|
|
18040
|
-
findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root),
|
|
18910
|
+
findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname12(root)));
|
|
18041
18911
|
continue;
|
|
18042
18912
|
}
|
|
18043
18913
|
if (!stat.isDirectory())
|
|
18044
18914
|
continue;
|
|
18045
18915
|
const tracked = trackedFiles(root);
|
|
18046
18916
|
for (const file of collectRepoFiles(root)) {
|
|
18047
|
-
const rel = toPosix(
|
|
18917
|
+
const rel = toPosix(relative8(root, file));
|
|
18048
18918
|
const isTracked = tracked.has(rel);
|
|
18049
18919
|
const text = readTextFile(file);
|
|
18050
18920
|
if (text === null)
|
|
@@ -18054,10 +18924,10 @@ function scanPackageManagerSecrets(options = {}) {
|
|
|
18054
18924
|
}
|
|
18055
18925
|
}
|
|
18056
18926
|
if (options.includeHome) {
|
|
18057
|
-
const home =
|
|
18927
|
+
const home = homedir10();
|
|
18058
18928
|
for (const name of HOME_FILES) {
|
|
18059
|
-
const file =
|
|
18060
|
-
if (!
|
|
18929
|
+
const file = join21(home, name);
|
|
18930
|
+
if (!existsSync19(file))
|
|
18061
18931
|
continue;
|
|
18062
18932
|
const text = readTextFile(file);
|
|
18063
18933
|
if (text === null)
|
|
@@ -18081,12 +18951,12 @@ function collectRepoFiles(root) {
|
|
|
18081
18951
|
if (entry.isDirectory()) {
|
|
18082
18952
|
if (SKIP_DIRS.has(entry.name))
|
|
18083
18953
|
continue;
|
|
18084
|
-
visit(
|
|
18954
|
+
visit(join21(dir, entry.name));
|
|
18085
18955
|
continue;
|
|
18086
18956
|
}
|
|
18087
18957
|
if (!entry.isFile())
|
|
18088
18958
|
continue;
|
|
18089
|
-
const file =
|
|
18959
|
+
const file = join21(dir, entry.name);
|
|
18090
18960
|
if (shouldScanRepoFile(file))
|
|
18091
18961
|
out.push(file);
|
|
18092
18962
|
}
|
|
@@ -18124,7 +18994,7 @@ function readTextFile(file) {
|
|
|
18124
18994
|
const stat = lstatSync7(file);
|
|
18125
18995
|
if (!stat.isFile() || stat.size > 5000000)
|
|
18126
18996
|
return null;
|
|
18127
|
-
const buf =
|
|
18997
|
+
const buf = readFileSync17(file);
|
|
18128
18998
|
if (buf.includes(0))
|
|
18129
18999
|
return null;
|
|
18130
19000
|
return buf.toString("utf-8");
|
|
@@ -18324,11 +19194,11 @@ function trackedFiles(root) {
|
|
|
18324
19194
|
}
|
|
18325
19195
|
function isTrackedFile(file) {
|
|
18326
19196
|
try {
|
|
18327
|
-
const repoRoot = execFileSync2("git", ["-C",
|
|
19197
|
+
const repoRoot = execFileSync2("git", ["-C", dirname12(file), "rev-parse", "--show-toplevel"], {
|
|
18328
19198
|
encoding: "utf-8",
|
|
18329
19199
|
stdio: ["ignore", "pipe", "ignore"]
|
|
18330
19200
|
}).trim();
|
|
18331
|
-
const rel = toPosix(
|
|
19201
|
+
const rel = toPosix(relative8(repoRoot, file));
|
|
18332
19202
|
execFileSync2("git", ["-C", repoRoot, "ls-files", "--error-unmatch", "--", rel], {
|
|
18333
19203
|
stdio: ["ignore", "ignore", "ignore"]
|
|
18334
19204
|
});
|
|
@@ -18354,13 +19224,13 @@ function stripInlineComment(value) {
|
|
|
18354
19224
|
return value.replace(/\s[#;].*$/, "").trim();
|
|
18355
19225
|
}
|
|
18356
19226
|
function displayPath(file, root) {
|
|
18357
|
-
const home =
|
|
19227
|
+
const home = homedir10();
|
|
18358
19228
|
if (root === home && (file === home || file.startsWith(home + "/")))
|
|
18359
|
-
return "~/" + toPosix(
|
|
18360
|
-
if (
|
|
18361
|
-
return toPosix(
|
|
19229
|
+
return "~/" + toPosix(relative8(home, file));
|
|
19230
|
+
if (isAbsolute8(root) && file.startsWith(root + "/"))
|
|
19231
|
+
return toPosix(relative8(root, file));
|
|
18362
19232
|
if (file === home || file.startsWith(home + "/"))
|
|
18363
|
-
return "~/" + toPosix(
|
|
19233
|
+
return "~/" + toPosix(relative8(home, file));
|
|
18364
19234
|
return file;
|
|
18365
19235
|
}
|
|
18366
19236
|
function toPosix(path) {
|
|
@@ -18526,6 +19396,7 @@ export {
|
|
|
18526
19396
|
INSTRUCTION_ACTIVATION_MODES,
|
|
18527
19397
|
INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS,
|
|
18528
19398
|
INSTRUCTIONS_LOCAL_OPT_IN_ENV,
|
|
19399
|
+
INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
|
|
18529
19400
|
INBOX_CONVERSATIONS_MINIMUM_VERSION,
|
|
18530
19401
|
GLOBAL_AGENT_RULES_STANDARD_SLUG,
|
|
18531
19402
|
GLOBAL_AGENT_RULES_STANDARD_CONTENT,
|