@hasna/instructions 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +123 -6
  2. package/dist/chunks/{apply-ynfn1av7.js → apply-7svah2qm.js} +5 -3
  3. package/dist/chunks/{apply-sfzja1f3.js → apply-jp13sa1e.js} +6 -3
  4. package/dist/chunks/database-wstmr56z.js +23 -0
  5. package/dist/chunks/{index-85n5wnd8.js → index-1cnyjnd7.js} +5 -7
  6. package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
  7. package/dist/chunks/{index-v7dkb384.js → index-4pfq42t5.js} +129 -77
  8. package/dist/chunks/index-5d7zdftc.js +186 -0
  9. package/dist/chunks/{index-1hpwskwd.js → index-7m4j8a33.js} +205 -329
  10. package/dist/chunks/index-8rb7ng2y.js +148 -0
  11. package/dist/chunks/index-arrrx0ed.js +344 -0
  12. package/dist/chunks/{index-9p9gn7gh.js → index-bzyxvwcd.js} +5 -7
  13. package/dist/chunks/index-fpeyzn3n.js +186 -0
  14. package/dist/chunks/{index-axhwg61p.js → index-fwbs6tma.js} +15 -4
  15. package/dist/chunks/index-n3fjspr0.js +119 -0
  16. package/dist/chunks/{index-e9epbsvs.js → index-z3ma4aqk.js} +15 -4
  17. package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
  18. package/dist/chunks/{local-a9nen65z.js → local-5wvv5xv3.js} +2 -2
  19. package/dist/chunks/{local-0d8qrc92.js → local-zxa5dpf4.js} +13 -188
  20. package/dist/chunks/s3-backup-n2z4fwyh.js +240 -0
  21. package/dist/chunks/s3-config-hhght91k.js +14 -0
  22. package/dist/chunks/s3-object-store-90tgbh6k.js +14 -0
  23. package/dist/chunks/{sync-m46fc257.js → sync-2075deve.js} +5 -4
  24. package/dist/chunks/{sync-vy1yjvs6.js → sync-r8t7zsp3.js} +4 -4
  25. package/dist/chunks/{template-02wjvv05.js → template-fsfxpe4p.js} +1 -1
  26. package/dist/chunks/{template-ke972qqe.js → template-wgnzdn7h.js} +1 -1
  27. package/dist/cli/index.js +1011 -247
  28. package/dist/data/config-store.d.ts +1 -0
  29. package/dist/data/config-store.d.ts.map +1 -1
  30. package/dist/index.js +840 -215
  31. package/dist/lib/apply.d.ts +7 -0
  32. package/dist/lib/apply.d.ts.map +1 -1
  33. package/dist/lib/compact-output.d.ts +1 -0
  34. package/dist/lib/compact-output.d.ts.map +1 -1
  35. package/dist/lib/export.d.ts +22 -3
  36. package/dist/lib/export.d.ts.map +1 -1
  37. package/dist/lib/import.d.ts +18 -0
  38. package/dist/lib/import.d.ts.map +1 -1
  39. package/dist/lib/legacy-store-migration.d.ts +39 -0
  40. package/dist/lib/legacy-store-migration.d.ts.map +1 -0
  41. package/dist/lib/managed-skill-runtimes.d.ts +1 -0
  42. package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
  43. package/dist/lib/sync-dir.d.ts.map +1 -1
  44. package/dist/mcp/index.js +31 -20
  45. package/dist/sdk/index.d.ts +2 -0
  46. package/dist/sdk/index.d.ts.map +1 -1
  47. package/dist/sdk/index.js +205 -10
  48. package/dist/sdk/resolve.d.ts +1 -1
  49. package/dist/sdk/resolve.d.ts.map +1 -1
  50. package/dist/sdk/v1-client.d.ts +57 -0
  51. package/dist/sdk/v1-client.d.ts.map +1 -0
  52. package/dist/sdk/v1.generated.d.ts +212 -21
  53. package/dist/sdk/v1.generated.d.ts.map +1 -1
  54. package/dist/server/cloud.d.ts.map +1 -1
  55. package/dist/server/index.d.ts +13 -5
  56. package/dist/server/index.d.ts.map +1 -1
  57. package/dist/server/index.js +1180 -432
  58. package/dist/server/openapi.d.ts +1264 -92
  59. package/dist/server/openapi.d.ts.map +1 -1
  60. package/dist/server/v1.d.ts +25 -1
  61. package/dist/server/v1.d.ts.map +1 -1
  62. package/dist/storage/cloud-store.d.ts +57 -1
  63. package/dist/storage/cloud-store.d.ts.map +1 -1
  64. package/dist/storage/index.d.ts +7 -0
  65. package/dist/storage/index.d.ts.map +1 -0
  66. package/dist/storage/index.js +725 -0
  67. package/dist/storage/s3-backup.d.ts +76 -0
  68. package/dist/storage/s3-backup.d.ts.map +1 -0
  69. package/dist/storage/s3-config.d.ts +46 -0
  70. package/dist/storage/s3-config.d.ts.map +1 -0
  71. package/dist/storage/s3-object-store.d.ts +70 -0
  72. package/dist/storage/s3-object-store.d.ts.map +1 -0
  73. package/dist/types/index.d.ts +65 -3
  74. package/dist/types/index.d.ts.map +1 -1
  75. package/hasna.contract.json +110 -0
  76. package/migrations/0001_instructions.sql +63 -0
  77. package/migrations/0002_api_keys.sql +20 -0
  78. package/migrations/0003_idempotency_receipts.sql +24 -0
  79. package/package.json +22 -11
  80. 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
- // ../contracts/dist/client/transport.js
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
- // ../contracts/dist/client/storage.js
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";
@@ -13876,11 +13876,11 @@ var init_local = __esm(() => {
13876
13876
 
13877
13877
  // src/data/config-store.ts
13878
13878
  import { randomUUID as randomUUID3 } from "crypto";
13879
- function parseBoundedPagePayload(value, label) {
13879
+ function parseBoundedPagePayload(value, label, expected) {
13880
13880
  const page = value;
13881
13881
  const consumed = Number(page?.cursor) + (page?.items?.length ?? 0);
13882
13882
  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)) {
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) || expected !== undefined && Number(page.cursor) !== expected.cursor) {
13884
13884
  throw new Error(`${label} returned an invalid or truncated bounded-read envelope`);
13885
13885
  }
13886
13886
  return {
@@ -13892,7 +13892,7 @@ function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
13892
13892
  if (value && typeof value === "object") {
13893
13893
  const candidate = value;
13894
13894
  if ("items" in candidate || "total" in candidate || "complete" in candidate || "truncated" in candidate || "next_cursor" in candidate) {
13895
- return parseBoundedPagePayload(value, label);
13895
+ return parseBoundedPagePayload(value, label, normalizeBoundedReadOptions(options));
13896
13896
  }
13897
13897
  }
13898
13898
  if (!Array.isArray(legacyItems)) {
@@ -13902,6 +13902,57 @@ function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
13902
13902
  const page = boundedReadPage(legacyItems.slice(normalized.cursor, normalized.cursor + normalized.limit), legacyItems.length, normalized);
13903
13903
  return { ...page, source_bounded: false };
13904
13904
  }
13905
+ async function aggregateBoundedCollection(label, readPage, identity, options = {}) {
13906
+ let lastError = null;
13907
+ for (let attempt = 0;attempt < COLLECTION_READ_ATTEMPTS; attempt += 1) {
13908
+ try {
13909
+ const items = [];
13910
+ const seen = new Set;
13911
+ let expectedTotal = null;
13912
+ let cursor = 0;
13913
+ let previousIdentity = null;
13914
+ while (true) {
13915
+ const page = await readPage(cursor);
13916
+ if (page.cursor !== cursor) {
13917
+ throw new CollectionChangedWhilePagingError(label, `server returned cursor ${page.cursor} for requested cursor ${cursor}`);
13918
+ }
13919
+ if (expectedTotal === null)
13920
+ expectedTotal = page.total;
13921
+ else if (page.total !== expectedTotal) {
13922
+ throw new CollectionChangedWhilePagingError(label, `total changed from ${expectedTotal} to ${page.total}`);
13923
+ }
13924
+ for (const item of page.items) {
13925
+ const key = identity(item);
13926
+ if (!key)
13927
+ throw new CollectionChangedWhilePagingError(label, "an item had no stable identity");
13928
+ if (seen.has(key))
13929
+ throw new CollectionChangedWhilePagingError(label, `duplicate identity ${key}`);
13930
+ if (options.requireAscendingIdentity && page.source_bounded && previousIdentity !== null && key <= previousIdentity) {
13931
+ throw new CollectionChangedWhilePagingError(label, `identity order was not strictly increasing at ${key}`);
13932
+ }
13933
+ seen.add(key);
13934
+ previousIdentity = key;
13935
+ items.push(item);
13936
+ }
13937
+ if (page.complete) {
13938
+ if (items.length !== expectedTotal) {
13939
+ throw new CollectionChangedWhilePagingError(label, `received ${items.length} unique rows for total ${expectedTotal}`);
13940
+ }
13941
+ return items;
13942
+ }
13943
+ if (page.next_cursor === null || page.next_cursor <= cursor) {
13944
+ throw new CollectionChangedWhilePagingError(label, `cursor did not advance from ${cursor}`);
13945
+ }
13946
+ cursor = page.next_cursor;
13947
+ }
13948
+ } catch (error) {
13949
+ if (!(error instanceof CollectionChangedWhilePagingError))
13950
+ throw error;
13951
+ lastError = error;
13952
+ }
13953
+ }
13954
+ throw lastError ?? new CollectionChangedWhilePagingError(label, "read did not stabilize");
13955
+ }
13905
13956
  function isLocalOptIn(env = process.env) {
13906
13957
  return isInstructionsLocalOptIn(env);
13907
13958
  }
@@ -13980,15 +14031,7 @@ class LocalConfigStore {
13980
14031
  return (await localStoreModule()).pruneSnapshots(configId, keep, this.db);
13981
14032
  }
13982
14033
  async listProfiles() {
13983
- const profiles = [];
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
- }
14034
+ return aggregateBoundedCollection("profile list", (cursor) => this.listProfilesPage({ limit: 100, cursor }), (profile) => profile.id);
13992
14035
  }
13993
14036
  async listProfilesPage(options = {}) {
13994
14037
  return (await localStoreModule()).listProfilesPage(options, this.db);
@@ -13997,15 +14040,7 @@ class LocalConfigStore {
13997
14040
  return (await localStoreModule()).getProfile(idOrSlug, this.db);
13998
14041
  }
13999
14042
  async getProfileConfigs(idOrSlug) {
14000
- const configs = [];
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
- }
14043
+ return aggregateBoundedCollection("profile membership", (cursor) => this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor }), (config) => config.id);
14009
14044
  }
14010
14045
  async getProfileConfigsPage(idOrSlug, options = {}) {
14011
14046
  return (await localStoreModule()).getProfileConfigsPage(idOrSlug, options, this.db);
@@ -14086,7 +14121,8 @@ class CloudConfigStore {
14086
14121
  throw err;
14087
14122
  }
14088
14123
  }
14089
- async listConfigs(filter = {}) {
14124
+ async listConfigsPageRemote(filter, options) {
14125
+ const normalized = normalizeBoundedReadOptions(options);
14090
14126
  const params = new URLSearchParams;
14091
14127
  if (filter.category)
14092
14128
  params.set("category", filter.category);
@@ -14096,16 +14132,21 @@ class CloudConfigStore {
14096
14132
  params.set("kind", filter.kind);
14097
14133
  if (filter.search)
14098
14134
  params.set("search", filter.search);
14099
- const qs = params.toString();
14100
- const { data } = await this.request("GET", `/configs${qs ? `?${qs}` : ""}`);
14101
- let configs = data?.configs ?? [];
14135
+ params.set("limit", String(normalized.limit));
14136
+ params.set("cursor", String(normalized.cursor));
14137
+ const { data } = await this.request("GET", `/configs?${params.toString()}`);
14138
+ return parseBoundedOrLegacyPage(data, data?.configs, normalized, "config list");
14139
+ }
14140
+ async listConfigs(filter = {}) {
14141
+ const configs = await aggregateBoundedCollection("config list", (cursor) => this.listConfigsPageRemote(filter, { limit: 100, cursor }), (config) => config.id, { requireAscendingIdentity: true });
14142
+ let filtered = configs;
14102
14143
  if (filter.tags && filter.tags.length > 0) {
14103
- configs = configs.filter((c) => filter.tags.every((t) => c.tags.includes(t)));
14144
+ filtered = filtered.filter((config) => filter.tags.every((tag) => config.tags.includes(tag)));
14104
14145
  }
14105
14146
  if (filter.is_template !== undefined) {
14106
- configs = configs.filter((c) => c.is_template === filter.is_template);
14147
+ filtered = filtered.filter((config) => config.is_template === filter.is_template);
14107
14148
  }
14108
- return configs;
14149
+ return filtered;
14109
14150
  }
14110
14151
  async getConfig(idOrSlug) {
14111
14152
  const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
@@ -14136,8 +14177,11 @@ class CloudConfigStore {
14136
14177
  return data ?? { total: 0 };
14137
14178
  }
14138
14179
  async listSnapshots(configId) {
14139
- const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots`);
14140
- return data?.snapshots ?? [];
14180
+ return aggregateBoundedCollection("snapshot list", async (cursor) => {
14181
+ const normalized = normalizeBoundedReadOptions({ limit: 100, cursor });
14182
+ const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots?limit=${normalized.limit}&cursor=${normalized.cursor}`);
14183
+ return parseBoundedOrLegacyPage(data, data?.snapshots, normalized, "snapshot list");
14184
+ }, (snapshot) => snapshot.id);
14141
14185
  }
14142
14186
  async getSnapshot(id) {
14143
14187
  const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
@@ -14160,15 +14204,7 @@ class CloudConfigStore {
14160
14204
  return data?.pruned ?? 0;
14161
14205
  }
14162
14206
  async listProfiles() {
14163
- const profiles = [];
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
- }
14207
+ return aggregateBoundedCollection("profile list", (cursor) => this.listProfilesPage({ limit: 100, cursor }), (profile) => profile.id, { requireAscendingIdentity: true });
14172
14208
  }
14173
14209
  async listProfilesPage(options = {}) {
14174
14210
  const normalized = normalizeBoundedReadOptions(options);
@@ -14192,15 +14228,7 @@ class CloudConfigStore {
14192
14228
  return profile;
14193
14229
  }
14194
14230
  async getProfileConfigs(idOrSlug) {
14195
- const configs = [];
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
- }
14231
+ return aggregateBoundedCollection("profile membership", (cursor) => this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor }), (config) => config.id);
14204
14232
  }
14205
14233
  async getProfileConfigsPage(idOrSlug, options = {}) {
14206
14234
  const normalized = normalizeBoundedReadOptions(options);
@@ -14214,20 +14242,26 @@ class CloudConfigStore {
14214
14242
  return parseBoundedOrLegacyPage(data.configs, data.profile.configs, normalized, "profile membership");
14215
14243
  }
14216
14244
  async getProfileConfigBindings(idOrSlug) {
14217
- const { status, data } = await this.requestProfileRoute(idOrSlug, (profileId) => `/profiles/${encodeURIComponent(profileId)}/bindings`, (value) => Array.isArray(value?.bindings));
14218
- if (status === 404) {
14219
- const profile = await this.getProfile(idOrSlug);
14220
- const configs = await this.getProfileConfigs(idOrSlug);
14221
- return configs.map((config, sort_order) => ({
14222
- profile_id: profile.id,
14223
- config_id: config.id,
14224
- sort_order,
14225
- binding: legacyProfileConfigBinding()
14226
- }));
14227
- }
14228
- if (!data || !Array.isArray(data.bindings))
14229
- throw new ProfileNotFoundError(idOrSlug);
14230
- return data.bindings;
14245
+ let routeMissing = false;
14246
+ const bindings = await aggregateBoundedCollection("profile config bindings", async (cursor) => {
14247
+ const normalized = normalizeBoundedReadOptions({ limit: 100, cursor });
14248
+ 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))));
14249
+ if (status === 404) {
14250
+ routeMissing = true;
14251
+ return { ...boundedReadPage([], 0, normalized), source_bounded: false };
14252
+ }
14253
+ return parseBoundedOrLegacyPage(data, data?.bindings, normalized, "profile config bindings");
14254
+ }, (binding) => `${binding.profile_id}\x00${binding.config_id}`);
14255
+ if (!routeMissing)
14256
+ return bindings;
14257
+ const profile = await this.getProfile(idOrSlug);
14258
+ const configs = await this.getProfileConfigs(idOrSlug);
14259
+ return configs.map((config, sort_order) => ({
14260
+ profile_id: profile.id,
14261
+ config_id: config.id,
14262
+ sort_order,
14263
+ binding: legacyProfileConfigBinding()
14264
+ }));
14231
14265
  }
14232
14266
  async requestProfileRoute(idOrSlug, pathForId, isUsable) {
14233
14267
  const attempted = new Set;
@@ -14285,14 +14319,20 @@ class CloudConfigStore {
14285
14319
  await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
14286
14320
  }
14287
14321
  async getProfileAssetBindings(profileIdOrSlug) {
14288
- const { status, data } = await this.requestProfileRoute(profileIdOrSlug, (profileId) => `/profiles/${encodeURIComponent(profileId)}/assets`, (value) => Array.isArray(value?.assets));
14289
- if (status === 404) {
14290
- await this.getProfile(profileIdOrSlug);
14291
- return [];
14292
- }
14293
- if (!data || !Array.isArray(data.assets))
14294
- throw new ProfileNotFoundError(profileIdOrSlug);
14295
- return data.assets;
14322
+ let routeMissing = false;
14323
+ const assets = await aggregateBoundedCollection("profile asset bindings", async (cursor) => {
14324
+ const normalized = normalizeBoundedReadOptions({ limit: 100, cursor });
14325
+ 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))));
14326
+ if (status === 404) {
14327
+ routeMissing = true;
14328
+ return { ...boundedReadPage([], 0, normalized), source_bounded: false };
14329
+ }
14330
+ return parseBoundedOrLegacyPage(data, data?.assets, normalized, "profile asset bindings");
14331
+ }, (asset) => `${asset.profile_id}\x00${asset.binding.assetKey}`);
14332
+ if (!routeMissing)
14333
+ return assets;
14334
+ await this.getProfile(profileIdOrSlug);
14335
+ return [];
14296
14336
  }
14297
14337
  async addAssetToProfile(profileIdOrSlug, sourceConfigId, binding) {
14298
14338
  const { data } = await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/assets`, { source_config_id: sourceConfigId, binding }, { idempotent: true });
@@ -14362,8 +14402,11 @@ class CloudConfigStore {
14362
14402
  await this.request("POST", "/machines/applied", { hostname: hostname2 });
14363
14403
  }
14364
14404
  async listMachines() {
14365
- const { data } = await this.request("GET", "/machines");
14366
- return data?.machines ?? [];
14405
+ return aggregateBoundedCollection("machine list", async (cursor) => {
14406
+ const normalized = normalizeBoundedReadOptions({ limit: 100, cursor });
14407
+ const { data } = await this.request("GET", `/machines?limit=${normalized.limit}&cursor=${normalized.cursor}`);
14408
+ return parseBoundedOrLegacyPage(data, data?.machines, normalized, "machine list");
14409
+ }, (machine) => machine.id, { requireAscendingIdentity: true });
14367
14410
  }
14368
14411
  async sendFeedback(input) {
14369
14412
  await this.request("POST", "/feedback", {
@@ -14386,13 +14429,19 @@ function resolveConfigStore(env = process.env, options = {}) {
14386
14429
  const { client } = resolveInstructionsStorageClient(env, options);
14387
14430
  return new CloudConfigStore(client);
14388
14431
  }
14389
- var LOCAL_OPT_IN_ENV, localStoreModulePromise = null;
14432
+ var CollectionChangedWhilePagingError, COLLECTION_READ_ATTEMPTS = 2, LOCAL_OPT_IN_ENV, localStoreModulePromise = null;
14390
14433
  var init_config_store = __esm(() => {
14391
14434
  init_types();
14392
14435
  init_bounded_read();
14393
14436
  init_instruction_graph();
14394
14437
  init_transport_resolver();
14395
14438
  init_local_opt_in();
14439
+ CollectionChangedWhilePagingError = class CollectionChangedWhilePagingError extends Error {
14440
+ constructor(label, detail) {
14441
+ super(`${label} changed while paging: ${detail}`);
14442
+ this.name = "CollectionChangedWhilePagingError";
14443
+ }
14444
+ };
14396
14445
  LOCAL_OPT_IN_ENV = INSTRUCTIONS_LOCAL_OPT_IN_ENV;
14397
14446
  });
14398
14447
 
@@ -14415,7 +14464,7 @@ init_config_agents();
14415
14464
  init_machine();
14416
14465
  init_session_render();
14417
14466
  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";
14467
+ 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
14468
  import { homedir as homedir7 } from "os";
14420
14469
 
14421
14470
  // src/lib/session-render-ownership.ts
@@ -14504,6 +14553,17 @@ function expandPath(p) {
14504
14553
  }
14505
14554
  return resolve9(p);
14506
14555
  }
14556
+ function compactPathForConfigHome(p) {
14557
+ const absolute = resolve9(p);
14558
+ const configHome = resolve9(getConfigHome());
14559
+ const conventionalHome = resolve9(process.env["HOME"] || homedir7());
14560
+ if (configHome !== conventionalHome)
14561
+ return absolute;
14562
+ const rel = relative4(configHome, absolute);
14563
+ if (!rel || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute6(rel))
14564
+ return absolute;
14565
+ return `~/${rel}`;
14566
+ }
14507
14567
  function normalizeTargetPath(p) {
14508
14568
  const expanded = expandPath(p);
14509
14569
  try {
@@ -14957,7 +15017,7 @@ import {
14957
15017
  writeFileSync as writeFileSync3
14958
15018
  } from "fs";
14959
15019
  import { homedir as homedir8 } from "os";
14960
- import { dirname as dirname8, join as join14, parse as parse4, relative as relative4, resolve as resolve10 } from "path";
15020
+ import { dirname as dirname8, join as join14, parse as parse4, relative as relative5, resolve as resolve10 } from "path";
14961
15021
  var INBOX_CONVERSATIONS_MINIMUM_VERSION = "0.5.28";
14962
15022
  var INBOX_SKILL_MARKERS = [
14963
15023
  [".claude", "skills", "inbox", "SKILL.md"],
@@ -14981,7 +15041,7 @@ function findSymlinkedAncestor(path) {
14981
15041
  const normalized = resolve10(path);
14982
15042
  const parsed = parse4(normalized);
14983
15043
  let current = parsed.root;
14984
- const rel = relative4(parsed.root, normalized);
15044
+ const rel = relative5(parsed.root, normalized);
14985
15045
  for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
14986
15046
  current = join14(current, segment);
14987
15047
  if (!existsSync11(current))
@@ -15000,16 +15060,7 @@ function assertNoSymlinkAncestors2(path) {
15000
15060
  function packagedInboxSkillPath(explicitPath) {
15001
15061
  if (explicitPath)
15002
15062
  return explicitPath;
15003
- const candidates = [
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;
15063
+ throw new Error("Bundled skill contracts are retired; use the Skills CLI to manage private skills");
15013
15064
  }
15014
15065
  function readCanonicalSkill(explicitPath) {
15015
15066
  const assetPath = packagedInboxSkillPath(explicitPath);
@@ -16028,7 +16079,7 @@ import {
16028
16079
  readdirSync as readdirSync3,
16029
16080
  statSync as statSync5
16030
16081
  } from "fs";
16031
- import { dirname as dirname10, isAbsolute as isAbsolute6, join as join17, parse as parse5, relative as relative5, resolve as resolve11 } from "path";
16082
+ import { dirname as dirname10, isAbsolute as isAbsolute7, join as join17, parse as parse5, relative as relative6, resolve as resolve11 } from "path";
16032
16083
 
16033
16084
  class SessionApplyError extends Error {
16034
16085
  constructor(message) {
@@ -16166,7 +16217,7 @@ function ensureSessionTargetHome(targetHome) {
16166
16217
  }
16167
16218
  function checkSessionRenderDrift(targetHome, manifestPath) {
16168
16219
  const safeTargetHome = assertSafeTargetHome(targetHome);
16169
- const resolvedManifestPath = manifestPath ? resolveManifestRelativePath(relative5(safeTargetHome, resolve11(manifestPath)), safeTargetHome) : resolve11(safeTargetHome, ".hasna", "session-render-manifest.json");
16220
+ const resolvedManifestPath = manifestPath ? resolveManifestRelativePath(relative6(safeTargetHome, resolve11(manifestPath)), safeTargetHome) : resolve11(safeTargetHome, ".hasna", "session-render-manifest.json");
16170
16221
  const checkedAt = new Date().toISOString();
16171
16222
  const previousManifest = readPreviousManifest(resolvedManifestPath);
16172
16223
  if (!previousManifest) {
@@ -16218,11 +16269,11 @@ function restoreSessionRenderSnapshot(snapshotPath, options = {}) {
16218
16269
  const targetHome = assertSafeTargetHome(snapshot.targetHome);
16219
16270
  const resolvedSnapshotPath = resolve11(snapshotPath);
16220
16271
  const snapshotDir = getSessionRenderSnapshotDir(targetHome);
16221
- const snapshotDirRelative = relative5(snapshotDir, resolvedSnapshotPath);
16222
- const insideSnapshotDir = snapshotDirRelative !== ".." && !snapshotDirRelative.startsWith("../") && !isAbsolute6(snapshotDirRelative);
16272
+ const snapshotDirRelative = relative6(snapshotDir, resolvedSnapshotPath);
16273
+ const insideSnapshotDir = snapshotDirRelative !== ".." && !snapshotDirRelative.startsWith("../") && !isAbsolute7(snapshotDirRelative);
16223
16274
  if (!insideSnapshotDir) {
16224
- const snapshotRelativePath = relative5(targetHome, resolvedSnapshotPath);
16225
- if (snapshotRelativePath === "" || snapshotRelativePath === ".." || snapshotRelativePath.startsWith("../") || isAbsolute6(snapshotRelativePath)) {
16275
+ const snapshotRelativePath = relative6(targetHome, resolvedSnapshotPath);
16276
+ if (snapshotRelativePath === "" || snapshotRelativePath === ".." || snapshotRelativePath.startsWith("../") || isAbsolute7(snapshotRelativePath)) {
16226
16277
  throw new SessionApplyError("Session snapshot must be stored inside its session-render snapshot location.");
16227
16278
  }
16228
16279
  }
@@ -16421,7 +16472,7 @@ function readSessionRenderSnapshot(snapshotPath) {
16421
16472
  function reconstructPreRollbackLegacyV1Snapshot(snapshot, previousFiles, previousManifestFiles, targetHome, snapshotPath) {
16422
16473
  assertNoNewerSessionSnapshot(snapshotPath, snapshot.createdAt, targetHome);
16423
16474
  const manifestPath = resolve11(snapshot.manifestPath);
16424
- const manifestRelativePath = relative5(targetHome, manifestPath).replaceAll("\\", "/");
16475
+ const manifestRelativePath = relative6(targetHome, manifestPath).replaceAll("\\", "/");
16425
16476
  resolveSnapshotFilePath(manifestRelativePath, snapshot.manifestPath, targetHome);
16426
16477
  const manifestSha256 = currentSessionFileHash(manifestPath, targetHome);
16427
16478
  if (manifestSha256 === null) {
@@ -16754,8 +16805,8 @@ function isPreviouslyManaged(file, previousSha256, previousHashes, previousManif
16754
16805
  }
16755
16806
  function resolvePlannedFilePath(plan, file, targetHome) {
16756
16807
  const target = resolve11(targetHome, ...file.relativePath.split("/"));
16757
- const rel = relative5(targetHome, target);
16758
- if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute6(rel)) {
16808
+ const rel = relative6(targetHome, target);
16809
+ if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute7(rel)) {
16759
16810
  throw new SessionApplyError(`Session file escapes target home: ${file.relativePath}`);
16760
16811
  }
16761
16812
  if (resolve11(file.path) !== target) {
@@ -16766,8 +16817,8 @@ function resolvePlannedFilePath(plan, file, targetHome) {
16766
16817
  }
16767
16818
  function resolveManifestRelativePath(relativePath, targetHome) {
16768
16819
  const target = resolve11(targetHome, ...relativePath.split(/[\\/]+/));
16769
- const rel = relative5(targetHome, target);
16770
- if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute6(rel)) {
16820
+ const rel = relative6(targetHome, target);
16821
+ if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute7(rel)) {
16771
16822
  throw new SessionApplyError(`Session manifest file escapes target home: ${relativePath}`);
16772
16823
  }
16773
16824
  assertNoSymlinkSegments2(targetHome, target);
@@ -16811,7 +16862,7 @@ function applyPlannedFile(plan, file, targetHome, resultsByPath, coordination, a
16811
16862
  function assertExpectedSessionFileHash(path, targetHome, expectedHash) {
16812
16863
  const actualHash = currentSessionFileHash(path, targetHome);
16813
16864
  if (actualHash !== expectedHash) {
16814
- throw new SessionApplyError(`Session apply path changed after planning: ${relative5(targetHome, path)}`);
16865
+ throw new SessionApplyError(`Session apply path changed after planning: ${relative6(targetHome, path)}`);
16815
16866
  }
16816
16867
  }
16817
16868
  function currentSessionFileHash(path, targetHome) {
@@ -16896,7 +16947,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
16896
16947
  };
16897
16948
  }
16898
16949
  function assertSafeTargetHome(targetHome) {
16899
- if (!isAbsolute6(targetHome))
16950
+ if (!isAbsolute7(targetHome))
16900
16951
  throw new SessionApplyError(`Session target home must be absolute: ${targetHome}`);
16901
16952
  const normalized = resolve11(targetHome);
16902
16953
  if (normalized === parse5(normalized).root) {
@@ -16910,7 +16961,7 @@ function assertSafeTargetHome(targetHome) {
16910
16961
  }
16911
16962
  function assertNoSymlinkSegments2(root, target) {
16912
16963
  assertNoSymlinkAncestors3(root);
16913
- const rel = relative5(root, target);
16964
+ const rel = relative6(root, target);
16914
16965
  let current = root;
16915
16966
  for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
16916
16967
  current = join17(current, segment);
@@ -16923,7 +16974,7 @@ function assertNoSymlinkAncestors3(path) {
16923
16974
  const normalized = resolve11(path);
16924
16975
  const parsed = parse5(normalized);
16925
16976
  let current = parsed.root;
16926
- const rel = relative5(parsed.root, normalized);
16977
+ const rel = relative6(parsed.root, normalized);
16927
16978
  for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
16928
16979
  current = join17(current, segment);
16929
16980
  if (!existsSync15(current))
@@ -17268,8 +17319,7 @@ init_transforms();
17268
17319
  // src/lib/sync-dir.ts
17269
17320
  init_config_store();
17270
17321
  import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync15, statSync as statSync6 } from "fs";
17271
- import { join as join18, relative as relative6 } from "path";
17272
- import { homedir as homedir10 } from "os";
17322
+ import { join as join18, relative as relative7 } from "path";
17273
17323
  init_redact();
17274
17324
  var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
17275
17325
  function shouldSkip(p) {
@@ -17282,7 +17332,6 @@ async function syncFromDir(dir, opts = {}) {
17282
17332
  return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
17283
17333
  const files = opts.recursive !== false ? walkDir(absDir) : readdirSync4(absDir).map((f) => join18(absDir, f)).filter((f) => statSync6(f).isFile());
17284
17334
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
17285
- const home = homedir10();
17286
17335
  const allConfigs = await store.listConfigs();
17287
17336
  for (const file of files) {
17288
17337
  if (shouldSkip(file)) {
@@ -17295,12 +17344,12 @@ async function syncFromDir(dir, opts = {}) {
17295
17344
  result.skipped.push(file + " (too large)");
17296
17345
  continue;
17297
17346
  }
17298
- const targetPath = file.replace(home, "~");
17347
+ const targetPath = compactPathForConfigHome(file);
17299
17348
  const redacted = redactContent(content, redactFormatForTarget(targetPath, detectFormat(file)));
17300
17349
  const existing = allConfigs.find((c) => c.target_path === targetPath);
17301
17350
  if (!existing) {
17302
17351
  if (!opts.dryRun)
17303
- await store.createConfig({ name: relative6(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content: redacted.content });
17352
+ await store.createConfig({ name: relative7(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content: redacted.content });
17304
17353
  result.added++;
17305
17354
  } else if (existing.content !== redacted.content) {
17306
17355
  if (!opts.dryRun)
@@ -17317,9 +17366,8 @@ async function syncFromDir(dir, opts = {}) {
17317
17366
  }
17318
17367
  async function syncToDir(dir, opts = {}) {
17319
17368
  const store = opts.store ?? resolveConfigStore();
17320
- const home = homedir10();
17321
17369
  const absDir = expandPath(dir);
17322
- const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
17370
+ const normalized = dir.startsWith("~/") ? dir : compactPathForConfigHome(absDir);
17323
17371
  const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
17324
17372
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
17325
17373
  for (const config of configs) {
@@ -17869,112 +17917,688 @@ function detectFormat(filePath) {
17869
17917
  return "text";
17870
17918
  }
17871
17919
  // src/lib/export.ts
17920
+ init_types();
17872
17921
  init_config_store();
17873
- import { existsSync as existsSync18, mkdirSync as mkdirSync8, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
17874
- import { join as join20, resolve as resolve12 } from "path";
17922
+ import { createHash as createHash11 } from "crypto";
17923
+ import { existsSync as existsSync18, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
17924
+ import { dirname as dirname11, join as join20, resolve as resolve12 } from "path";
17875
17925
  import { tmpdir } from "os";
17876
- async function exportConfigs(outputPath, opts = {}) {
17926
+ var ARCHIVE_EXCLUSIONS = [
17927
+ {
17928
+ entity: "api_keys",
17929
+ classification: "security_state",
17930
+ reason: "API credentials are security state and must be provisioned independently, never copied in a domain archive."
17931
+ },
17932
+ {
17933
+ entity: "idempotency_receipts",
17934
+ classification: "transport_state",
17935
+ reason: "Request replay receipts are transport state and are not portable business-domain data."
17936
+ },
17937
+ {
17938
+ entity: "feedback",
17939
+ classification: "out_of_domain",
17940
+ reason: "Feedback is product telemetry, not part of the Instructions configuration domain."
17941
+ }
17942
+ ];
17943
+ function compareText(left, right) {
17944
+ return left < right ? -1 : left > right ? 1 : 0;
17945
+ }
17946
+ function sha25611(value) {
17947
+ return createHash11("sha256").update(value).digest("hex");
17948
+ }
17949
+ function canonicalize(value) {
17950
+ if (Array.isArray(value))
17951
+ return value.map(canonicalize);
17952
+ if (value && typeof value === "object") {
17953
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => compareText(left, right)).map(([key, entry]) => [key, canonicalize(entry)]));
17954
+ }
17955
+ return value;
17956
+ }
17957
+ function canonicalDomainJson(value) {
17958
+ return JSON.stringify(canonicalize(value));
17959
+ }
17960
+ function logicalAssetLocator(locator, idToSlug) {
17961
+ const match = /^config:\/\/([^@]+)@(\d+)$/.exec(locator);
17962
+ if (!match)
17963
+ return locator;
17964
+ let configId;
17965
+ try {
17966
+ configId = decodeURIComponent(match[1]);
17967
+ } catch {
17968
+ return locator;
17969
+ }
17970
+ const slug2 = idToSlug.get(configId);
17971
+ return slug2 ? `config-slug://${encodeURIComponent(slug2)}@${match[2]}` : locator;
17972
+ }
17973
+ function logicalCollections(domain, options) {
17974
+ const configs = [...domain.configs].sort((left, right) => compareText(left.slug, right.slug));
17975
+ const snapshots = [...domain.config_snapshots].sort((left, right) => compareText(left.config_slug, right.config_slug) || left.version - right.version || compareText(left.id, right.id));
17976
+ const profiles = [...domain.profiles].sort((left, right) => compareText(left.slug, right.slug));
17977
+ 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));
17978
+ 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));
17979
+ const machines = [...domain.machines].sort((left, right) => compareText(left.hostname, right.hostname));
17980
+ const idToSlug = new Map(configs.map((config) => [config.id, config.slug]));
17981
+ const configOrder = new Map;
17982
+ const assetOrder = new Map;
17983
+ return {
17984
+ configs: configs.map((config) => ({
17985
+ slug: config.slug,
17986
+ name: config.name,
17987
+ kind: config.kind,
17988
+ category: config.category,
17989
+ agent: config.agent,
17990
+ target_path: config.target_path,
17991
+ outputs: config.outputs,
17992
+ format: config.format,
17993
+ content: config.content,
17994
+ description: config.description,
17995
+ tags: [...config.tags].sort(compareText),
17996
+ is_template: config.is_template,
17997
+ version: config.version,
17998
+ ...options.includeOperationalTimestamps ? {
17999
+ created_at: config.created_at,
18000
+ updated_at: config.updated_at,
18001
+ synced_at: config.synced_at
18002
+ } : {}
18003
+ })),
18004
+ config_snapshots: snapshots.map((snapshot) => ({
18005
+ config_slug: snapshot.config_slug,
18006
+ version: snapshot.version,
18007
+ content: snapshot.content,
18008
+ ...options.includeOperationalTimestamps ? { created_at: snapshot.created_at } : {}
18009
+ })),
18010
+ profiles: profiles.map((profile) => ({
18011
+ slug: profile.slug,
18012
+ name: profile.name,
18013
+ description: profile.description,
18014
+ selectors: profile.selectors,
18015
+ variables: profile.variables,
18016
+ ...options.includeOperationalTimestamps ? {
18017
+ created_at: profile.created_at,
18018
+ updated_at: profile.updated_at
18019
+ } : {}
18020
+ })),
18021
+ profile_config_bindings: configBindings.map((row) => {
18022
+ const order = configOrder.get(row.profile_slug) ?? 0;
18023
+ configOrder.set(row.profile_slug, order + 1);
18024
+ return {
18025
+ profile_slug: row.profile_slug,
18026
+ config_slug: row.config_slug,
18027
+ order,
18028
+ binding: row.binding
18029
+ };
18030
+ }),
18031
+ profile_asset_bindings: assetBindings.map((row) => {
18032
+ const order = assetOrder.get(row.profile_slug) ?? 0;
18033
+ assetOrder.set(row.profile_slug, order + 1);
18034
+ return {
18035
+ profile_slug: row.profile_slug,
18036
+ source_config_slug: row.source_config_slug,
18037
+ order,
18038
+ binding: {
18039
+ ...row.binding,
18040
+ source: {
18041
+ ...row.binding.source,
18042
+ locator: logicalAssetLocator(row.binding.source.locator, idToSlug)
18043
+ }
18044
+ }
18045
+ };
18046
+ }),
18047
+ machines: machines.map((machine) => ({
18048
+ hostname: machine.hostname,
18049
+ os: machine.os,
18050
+ arch: machine.arch,
18051
+ ...options.includeOperationalTimestamps ? {
18052
+ created_at: machine.created_at,
18053
+ last_applied_at: machine.last_applied_at
18054
+ } : {
18055
+ applied: machine.last_applied_at !== null
18056
+ }
18057
+ }))
18058
+ };
18059
+ }
18060
+ function computeIntegrity(domain, options) {
18061
+ const collections = logicalCollections(domain, options);
18062
+ const counts = {
18063
+ configs: collections.configs.length,
18064
+ config_snapshots: collections.config_snapshots.length,
18065
+ profiles: collections.profiles.length,
18066
+ profile_config_bindings: collections.profile_config_bindings.length,
18067
+ profile_asset_bindings: collections.profile_asset_bindings.length,
18068
+ machines: collections.machines.length
18069
+ };
18070
+ const hashes = {
18071
+ configs: sha25611(canonicalDomainJson(collections.configs)),
18072
+ config_snapshots: sha25611(canonicalDomainJson(collections.config_snapshots)),
18073
+ profiles: sha25611(canonicalDomainJson(collections.profiles)),
18074
+ profile_config_bindings: sha25611(canonicalDomainJson(collections.profile_config_bindings)),
18075
+ profile_asset_bindings: sha25611(canonicalDomainJson(collections.profile_asset_bindings)),
18076
+ machines: sha25611(canonicalDomainJson(collections.machines))
18077
+ };
18078
+ return {
18079
+ algorithm: "sha256",
18080
+ canonicalization: options.canonicalization,
18081
+ counts,
18082
+ hashes,
18083
+ domain_sha256: sha25611(canonicalDomainJson({ counts, hashes }))
18084
+ };
18085
+ }
18086
+ function computeDomainIntegrity(domain) {
18087
+ return computeIntegrity(domain, {
18088
+ includeOperationalTimestamps: true,
18089
+ canonicalization: "hasna.instructions.logical-json/v1"
18090
+ });
18091
+ }
18092
+ function computeRestorableDomainIntegrity(domain) {
18093
+ return computeIntegrity(domain, {
18094
+ includeOperationalTimestamps: false,
18095
+ canonicalization: "hasna.instructions.restorable-logical-json/v1"
18096
+ });
18097
+ }
18098
+ function assertArray(value, label) {
18099
+ if (!Array.isArray(value))
18100
+ throw new Error(`Invalid v2 archive: ${label} must be an array`);
18101
+ }
18102
+ function assertUnique(values, label) {
18103
+ const seen = new Set;
18104
+ for (const value of values) {
18105
+ if (!value || seen.has(value))
18106
+ throw new Error(`Invalid v2 archive: duplicate or empty ${label} ${JSON.stringify(value)}`);
18107
+ seen.add(value);
18108
+ }
18109
+ }
18110
+ function assertTimestamp(value, label, nullable = false) {
18111
+ if (nullable && value === null)
18112
+ return;
18113
+ if (typeof value !== "string" || value.length === 0 || Number.isNaN(Date.parse(value))) {
18114
+ throw new Error(`Invalid v2 archive: ${label} must be ${nullable ? "null or " : ""}an ISO timestamp`);
18115
+ }
18116
+ }
18117
+ function validateInstructionsDomainArchive(value) {
18118
+ if (!value || typeof value !== "object")
18119
+ throw new Error("Invalid v2 archive: domain payload must be an object");
18120
+ const domain = value;
18121
+ if (domain.schema !== INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA)
18122
+ throw new Error("Invalid v2 archive: unsupported domain schema");
18123
+ assertArray(domain.configs, "configs");
18124
+ assertArray(domain.config_snapshots, "config_snapshots");
18125
+ assertArray(domain.profiles, "profiles");
18126
+ assertArray(domain.profile_config_bindings, "profile_config_bindings");
18127
+ assertArray(domain.profile_asset_bindings, "profile_asset_bindings");
18128
+ assertArray(domain.machines, "machines");
18129
+ const typed = domain;
18130
+ assertUnique(typed.configs.map((row) => row.slug), "config slug");
18131
+ assertUnique(typed.profiles.map((row) => row.slug), "profile slug");
18132
+ assertUnique(typed.machines.map((row) => row.hostname), "machine hostname");
18133
+ assertUnique(typed.config_snapshots.map((row) => `${row.config_slug}@${row.version}`), "config snapshot");
18134
+ assertUnique(typed.profile_config_bindings.map((row) => `${row.profile_slug}:${row.config_slug}`), "profile config binding");
18135
+ assertUnique(typed.profile_asset_bindings.map((row) => `${row.profile_slug}:${row.binding.assetKey}`), "profile asset binding");
18136
+ const configSlugs = new Set(typed.configs.map((row) => row.slug));
18137
+ const profileSlugs = new Set(typed.profiles.map((row) => row.slug));
18138
+ for (const config of typed.configs) {
18139
+ assertTimestamp(config.created_at, `config ${config.slug} created_at`);
18140
+ assertTimestamp(config.updated_at, `config ${config.slug} updated_at`);
18141
+ assertTimestamp(config.synced_at, `config ${config.slug} synced_at`, true);
18142
+ if (!Number.isSafeInteger(config.version) || config.version < 1)
18143
+ throw new Error(`Invalid v2 archive: config ${config.slug} has invalid version`);
18144
+ const snapshots = typed.config_snapshots.filter((row) => row.config_slug === config.slug).sort((a, b) => a.version - b.version);
18145
+ if (snapshots.some((row) => !Number.isSafeInteger(row.version) || row.version < 1 || row.version > config.version)) {
18146
+ throw new Error(`Invalid v2 archive: config ${config.slug} has an invalid snapshot version`);
18147
+ }
18148
+ if (snapshots.length) {
18149
+ const first = snapshots[0].version;
18150
+ const expected = Array.from({ length: config.version - first + 1 }, (_, index) => first + index);
18151
+ if (canonicalDomainJson(snapshots.map((row) => row.version)) !== canonicalDomainJson(expected) || snapshots.at(-1).version !== config.version) {
18152
+ throw new Error(`Invalid v2 archive: snapshots for ${config.slug} must be a contiguous suffix ending at version ${config.version}`);
18153
+ }
18154
+ if (snapshots.at(-1).content !== config.content) {
18155
+ throw new Error(`Invalid v2 archive: latest snapshot content differs from config ${config.slug}`);
18156
+ }
18157
+ }
18158
+ }
18159
+ for (const row of typed.config_snapshots) {
18160
+ if (!configSlugs.has(row.config_slug))
18161
+ throw new Error(`Invalid v2 archive: snapshot references missing config ${row.config_slug}`);
18162
+ assertTimestamp(row.created_at, `snapshot ${row.config_slug}@${row.version} created_at`);
18163
+ }
18164
+ for (const profile of typed.profiles) {
18165
+ assertTimestamp(profile.created_at, `profile ${profile.slug} created_at`);
18166
+ assertTimestamp(profile.updated_at, `profile ${profile.slug} updated_at`);
18167
+ }
18168
+ for (const row of typed.profile_config_bindings) {
18169
+ if (!profileSlugs.has(row.profile_slug) || !configSlugs.has(row.config_slug)) {
18170
+ throw new Error(`Invalid v2 archive: profile config binding has a missing logical reference`);
18171
+ }
18172
+ }
18173
+ for (const row of typed.profile_asset_bindings) {
18174
+ if (!profileSlugs.has(row.profile_slug) || !configSlugs.has(row.source_config_slug)) {
18175
+ throw new Error(`Invalid v2 archive: profile asset binding has a missing logical reference`);
18176
+ }
18177
+ }
18178
+ for (const machine of typed.machines) {
18179
+ assertTimestamp(machine.created_at, `machine ${machine.hostname} created_at`);
18180
+ assertTimestamp(machine.last_applied_at, `machine ${machine.hostname} last_applied_at`, true);
18181
+ }
18182
+ return typed;
18183
+ }
18184
+ function matchesFilter(config, filter) {
18185
+ if (!filter)
18186
+ return true;
18187
+ if (filter.category && config.category !== filter.category)
18188
+ return false;
18189
+ if (filter.agent && config.agent !== filter.agent)
18190
+ return false;
18191
+ if (filter.kind && config.kind !== filter.kind)
18192
+ return false;
18193
+ if (filter.is_template !== undefined && config.is_template !== filter.is_template)
18194
+ return false;
18195
+ if (filter.tags?.length && !filter.tags.every((tag) => config.tags.includes(tag)))
18196
+ return false;
18197
+ if (filter.search) {
18198
+ const needle = filter.search.toLocaleLowerCase();
18199
+ if (![config.name, config.description ?? "", config.content].some((value) => value.toLocaleLowerCase().includes(needle)))
18200
+ return false;
18201
+ }
18202
+ return true;
18203
+ }
18204
+ async function collectInstructionsDomain(opts = {}) {
17877
18205
  const store = opts.store ?? resolveConfigStore();
17878
- const configs = await store.listConfigs(opts.filter);
18206
+ const selectedProfile = opts.profileId ? await store.getProfile(opts.profileId) : null;
18207
+ const listedConfigs = selectedProfile ? await store.getProfileConfigs(selectedProfile.id) : await store.listConfigs(opts.filter);
18208
+ const configs = listedConfigs.filter((config) => matchesFilter(config, selectedProfile ? opts.filter : undefined));
18209
+ configs.sort((left, right) => compareText(left.slug, right.slug));
18210
+ const selectedIds = new Set(configs.map((config) => config.id));
18211
+ const profiles = (selectedProfile ? [selectedProfile] : await store.listProfiles()).sort((left, right) => compareText(left.slug, right.slug));
18212
+ const profileById = new Map(profiles.map((profile) => [profile.id, profile]));
18213
+ const configById = new Map(configs.map((config) => [config.id, config]));
18214
+ const configSnapshots = [];
18215
+ for (const config of configs) {
18216
+ for (const snapshot of await store.listSnapshots(config.id)) {
18217
+ configSnapshots.push({
18218
+ id: snapshot.id,
18219
+ config_slug: config.slug,
18220
+ content: snapshot.content,
18221
+ version: snapshot.version,
18222
+ created_at: snapshot.created_at
18223
+ });
18224
+ }
18225
+ }
18226
+ configSnapshots.sort((left, right) => compareText(left.config_slug, right.config_slug) || left.version - right.version || compareText(left.id, right.id));
18227
+ const profileConfigBindings = (await Promise.all(profiles.map(async (profile) => (await store.getProfileConfigBindings(profile.id)).filter((row) => selectedIds.has(row.config_id)).map((row) => ({
18228
+ profile_slug: profile.slug,
18229
+ config_slug: configById.get(row.config_id).slug,
18230
+ sort_order: row.sort_order,
18231
+ binding: row.binding
18232
+ }))))).flat().sort((left, right) => compareText(left.profile_slug, right.profile_slug) || left.sort_order - right.sort_order || compareText(left.config_slug, right.config_slug));
18233
+ const profileAssetBindings = (await Promise.all(profiles.map(async (profile) => (await store.getProfileAssetBindings(profile.id)).filter((row) => selectedIds.has(row.source_config_id)).map((row) => ({
18234
+ profile_slug: profile.slug,
18235
+ source_config_slug: configById.get(row.source_config_id).slug,
18236
+ sort_order: row.sort_order,
18237
+ binding: row.binding
18238
+ }))))).flat().sort((left, right) => compareText(left.profile_slug, right.profile_slug) || left.sort_order - right.sort_order || compareText(left.binding.assetKey, right.binding.assetKey));
18239
+ const archivedProfiles = profiles.filter((profile) => profileById.has(profile.id));
18240
+ const machines = (await store.listMachines()).sort((left, right) => compareText(left.hostname, right.hostname));
18241
+ return {
18242
+ schema: INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
18243
+ configs,
18244
+ config_snapshots: configSnapshots,
18245
+ profiles: archivedProfiles,
18246
+ profile_config_bindings: profileConfigBindings,
18247
+ profile_asset_bindings: profileAssetBindings,
18248
+ machines
18249
+ };
18250
+ }
18251
+ async function exportConfigs(outputPath, opts = {}) {
18252
+ const domain = validateInstructionsDomainArchive(await collectInstructionsDomain(opts));
18253
+ const integrity = computeDomainIntegrity(domain);
18254
+ const payload = `${JSON.stringify(domain, null, 2)}
18255
+ `;
18256
+ const manifest = {
18257
+ schema: INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
18258
+ version: "2.0.0",
18259
+ exported_at: new Date().toISOString(),
18260
+ payload: {
18261
+ path: "domain.json",
18262
+ sha256: sha25611(payload),
18263
+ size_bytes: Buffer.byteLength(payload)
18264
+ },
18265
+ integrity,
18266
+ exclusions: ARCHIVE_EXCLUSIONS
18267
+ };
17879
18268
  const absOutput = resolve12(outputPath);
17880
- const tmpDir = join20(tmpdir(), `configs-export-${Date.now()}`);
17881
- const contentsDir = join20(tmpDir, "contents");
18269
+ mkdirSync8(dirname11(absOutput), { recursive: true });
18270
+ const stagingDir = mkdtempSync(join20(tmpdir(), "instructions-domain-export-"));
17882
18271
  try {
17883
- mkdirSync8(contentsDir, { recursive: true });
17884
- const manifest = {
17885
- version: "1.0.0",
17886
- exported_at: new Date().toISOString(),
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, "."], {
18272
+ writeFileSync6(join20(stagingDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
18273
+ `, "utf-8");
18274
+ writeFileSync6(join20(stagingDir, "domain.json"), payload, "utf-8");
18275
+ const proc = Bun.spawn(["tar", "czf", absOutput, "-C", stagingDir, "manifest.json", "domain.json"], {
17895
18276
  stdout: "pipe",
17896
18277
  stderr: "pipe"
17897
18278
  });
17898
18279
  const exitCode = await proc.exited;
17899
18280
  if (exitCode !== 0) {
17900
18281
  const stderr = await new Response(proc.stderr).text();
17901
- throw new Error(`tar failed: ${stderr}`);
18282
+ throw new Error(`tar failed: ${stderr.trim()}`);
17902
18283
  }
17903
- return { path: absOutput, count: configs.length };
18284
+ return { path: absOutput, count: domain.configs.length, counts: integrity.counts, integrity };
17904
18285
  } finally {
17905
- if (existsSync18(tmpDir)) {
17906
- rmSync4(tmpDir, { recursive: true, force: true });
17907
- }
18286
+ if (existsSync18(stagingDir))
18287
+ rmSync4(stagingDir, { recursive: true, force: true });
17908
18288
  }
17909
18289
  }
17910
18290
  // src/lib/import.ts
18291
+ init_types();
17911
18292
  init_config_store();
17912
- import { existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync17, rmSync as rmSync5 } from "fs";
17913
- import { join as join21, resolve as resolve13 } from "path";
17914
- import { tmpdir as tmpdir2 } from "os";
17915
- async function importConfigs(bundlePath, opts = {}) {
17916
- const store = opts.store ?? resolveConfigStore();
17917
- const conflict = opts.conflict ?? "skip";
17918
- const absPath = resolve13(bundlePath);
17919
- const tmpDir = join21(tmpdir2(), `configs-import-${Date.now()}`);
17920
- const result = { created: 0, updated: 0, skipped: 0, errors: [] };
18293
+ import { createHash as createHash12 } from "crypto";
18294
+ import { resolve as resolve13 } from "path";
18295
+ function compareText2(left, right) {
18296
+ return left < right ? -1 : left > right ? 1 : 0;
18297
+ }
18298
+ function emptyMutationCounts() {
18299
+ return { created: 0, updated: 0, skipped: 0 };
18300
+ }
18301
+ function emptyResult() {
18302
+ return {
18303
+ created: 0,
18304
+ updated: 0,
18305
+ skipped: 0,
18306
+ counts: {
18307
+ configs: emptyMutationCounts(),
18308
+ config_snapshots: { created: 0, skipped: 0 },
18309
+ profiles: emptyMutationCounts(),
18310
+ profile_config_bindings: emptyMutationCounts(),
18311
+ profile_asset_bindings: emptyMutationCounts(),
18312
+ machines: emptyMutationCounts()
18313
+ },
18314
+ errors: [],
18315
+ integrity: null
18316
+ };
18317
+ }
18318
+ function sha25612(value) {
18319
+ return createHash12("sha256").update(value).digest("hex");
18320
+ }
18321
+ function normalizeMemberName(name) {
18322
+ let normalized = name;
18323
+ while (normalized.startsWith("./"))
18324
+ normalized = normalized.slice(2);
18325
+ if (normalized.startsWith("/") || normalized.includes("\x00") || normalized.split("/").some((segment) => segment === "..")) {
18326
+ throw new Error(`Invalid archive member path: ${JSON.stringify(name)}`);
18327
+ }
18328
+ return normalized;
18329
+ }
18330
+ async function openArchive(path) {
18331
+ const list = Bun.spawn(["tar", "tzf", path], { stdout: "pipe", stderr: "pipe" });
18332
+ const [exitCode, stdout, stderr] = await Promise.all([
18333
+ list.exited,
18334
+ new Response(list.stdout).text(),
18335
+ new Response(list.stderr).text()
18336
+ ]);
18337
+ if (exitCode !== 0)
18338
+ throw new Error(`tar listing failed: ${stderr.trim()}`);
18339
+ const members = new Map;
18340
+ for (const raw of stdout.split(`
18341
+ `).filter(Boolean)) {
18342
+ const normalized = normalizeMemberName(raw);
18343
+ if (normalized.length === 0)
18344
+ continue;
18345
+ if (members.has(normalized))
18346
+ throw new Error(`Invalid archive: duplicate member ${normalized}`);
18347
+ members.set(normalized, raw);
18348
+ }
18349
+ return {
18350
+ members,
18351
+ async read(member) {
18352
+ const actual = members.get(member);
18353
+ if (!actual)
18354
+ throw new Error(`Invalid archive: missing ${member}`);
18355
+ const proc = Bun.spawn(["tar", "xOzf", path, "--", actual], { stdout: "pipe", stderr: "pipe" });
18356
+ const [code, content, memberError] = await Promise.all([
18357
+ proc.exited,
18358
+ new Response(proc.stdout).text(),
18359
+ new Response(proc.stderr).text()
18360
+ ]);
18361
+ if (code !== 0)
18362
+ throw new Error(`tar read failed for ${member}: ${memberError.trim()}`);
18363
+ return content;
18364
+ }
18365
+ };
18366
+ }
18367
+ function isNotFound(error) {
18368
+ return Boolean(error && typeof error === "object" && (error.name === "ConfigNotFoundError" || error.name === "ProfileNotFoundError" || error.status === 404));
18369
+ }
18370
+ async function findConfig(store, slug2) {
17921
18371
  try {
17922
- mkdirSync9(tmpDir, { recursive: true });
17923
- const proc = Bun.spawn(["tar", "xzf", absPath, "-C", tmpDir], {
17924
- stdout: "pipe",
17925
- stderr: "pipe"
18372
+ return await store.getConfig(slug2);
18373
+ } catch (error) {
18374
+ if (isNotFound(error))
18375
+ return null;
18376
+ throw error;
18377
+ }
18378
+ }
18379
+ function verifyCounts(actual, expected) {
18380
+ if (canonicalDomainJson(actual) !== canonicalDomainJson(expected)) {
18381
+ throw new Error("Archive integrity failure: manifest counts do not match the domain payload");
18382
+ }
18383
+ }
18384
+ async function readV2(reader, manifest) {
18385
+ 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") {
18386
+ throw new Error("Invalid v2 archive manifest");
18387
+ }
18388
+ const payload = await reader.read("domain.json");
18389
+ if (Buffer.byteLength(payload) !== manifest.payload.size_bytes || sha25612(payload) !== manifest.payload.sha256) {
18390
+ throw new Error("Archive integrity failure: domain payload hash or size does not match the manifest");
18391
+ }
18392
+ const domain = validateInstructionsDomainArchive(JSON.parse(payload));
18393
+ const actual = computeDomainIntegrity(domain);
18394
+ verifyCounts(actual.counts, manifest.integrity.counts);
18395
+ if (canonicalDomainJson(actual) !== canonicalDomainJson(manifest.integrity)) {
18396
+ throw new Error("Archive integrity failure: logical domain hashes do not match the manifest");
18397
+ }
18398
+ return domain;
18399
+ }
18400
+ function configInput(config, content) {
18401
+ return {
18402
+ name: config.name,
18403
+ kind: config.kind,
18404
+ category: config.category,
18405
+ agent: config.agent,
18406
+ target_path: config.target_path,
18407
+ outputs: config.outputs,
18408
+ format: config.format,
18409
+ content,
18410
+ description: config.description ?? undefined,
18411
+ tags: config.tags,
18412
+ is_template: config.is_template
18413
+ };
18414
+ }
18415
+ async function restoreNewConfig(store, archived, domain) {
18416
+ const snapshots = domain.config_snapshots.filter((row) => row.config_slug === archived.slug).sort((left, right) => left.version - right.version);
18417
+ const byVersion = new Map(snapshots.map((snapshot) => [snapshot.version, snapshot.content]));
18418
+ let content = byVersion.get(1) ?? snapshots[0]?.content ?? archived.content;
18419
+ let restored = await store.createConfig(configInput(archived, content));
18420
+ for (let version = 2;version <= archived.version; version++) {
18421
+ content = byVersion.get(version) ?? (version === archived.version ? archived.content : content);
18422
+ restored = await store.updateConfig(restored.id, {
18423
+ content,
18424
+ ...version === archived.version ? {
18425
+ name: archived.name,
18426
+ kind: archived.kind,
18427
+ category: archived.category,
18428
+ agent: archived.agent,
18429
+ target_path: archived.target_path,
18430
+ outputs: archived.outputs,
18431
+ format: archived.format,
18432
+ description: archived.description,
18433
+ tags: archived.tags,
18434
+ is_template: archived.is_template,
18435
+ synced_at: archived.synced_at
18436
+ } : {}
17926
18437
  });
17927
- const exitCode = await proc.exited;
17928
- if (exitCode !== 0) {
17929
- const stderr = await new Response(proc.stderr).text();
17930
- throw new Error(`tar extraction failed: ${stderr}`);
18438
+ }
18439
+ await store.pruneSnapshots(restored.id, snapshots.length);
18440
+ const restoredSnapshots = (await store.listSnapshots(restored.id)).map(({ version, content: snapshotContent }) => ({ version, content: snapshotContent })).sort((left, right) => left.version - right.version);
18441
+ const expectedSnapshots = snapshots.map(({ version, content: snapshotContent }) => ({ version, content: snapshotContent }));
18442
+ if (restored.version !== archived.version || restored.content !== archived.content || canonicalDomainJson(restoredSnapshots) !== canonicalDomainJson(expectedSnapshots)) {
18443
+ throw new Error(`restored config or snapshot history did not match archive for ${archived.slug}`);
18444
+ }
18445
+ return restored;
18446
+ }
18447
+ function rewriteAssetLocator(binding, archivedSourceId, restoredSourceId) {
18448
+ const match = /^config:\/\/([^@]+)@(\d+)$/.exec(binding.source.locator);
18449
+ if (!match)
18450
+ return binding;
18451
+ let locatorId;
18452
+ try {
18453
+ locatorId = decodeURIComponent(match[1]);
18454
+ } catch {
18455
+ return binding;
18456
+ }
18457
+ if (locatorId !== archivedSourceId)
18458
+ return binding;
18459
+ return {
18460
+ ...binding,
18461
+ source: {
18462
+ ...binding.source,
18463
+ locator: `config://${encodeURIComponent(restoredSourceId)}@${match[2]}`
17931
18464
  }
17932
- const manifestPath = join21(tmpDir, "manifest.json");
17933
- if (!existsSync19(manifestPath))
17934
- throw new Error("Invalid bundle: missing manifest.json");
17935
- const manifest = JSON.parse(readFileSync17(manifestPath, "utf-8"));
17936
- for (const meta of manifest.configs) {
17937
- try {
17938
- const ext = meta.format === "text" ? "txt" : meta.format;
17939
- const contentFile = join21(tmpDir, "contents", `${meta.slug}.${ext}`);
17940
- const content = existsSync19(contentFile) ? readFileSync17(contentFile, "utf-8") : "";
17941
- let existing = null;
17942
- try {
17943
- existing = await store.getConfig(meta.slug);
17944
- } catch {}
17945
- if (existing) {
17946
- if (conflict === "skip") {
17947
- result.skipped++;
17948
- } else if (conflict === "overwrite" || conflict === "version") {
17949
- await store.updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs });
17950
- result.updated++;
17951
- }
17952
- } else {
17953
- await store.createConfig({
17954
- name: meta.name,
17955
- kind: meta.kind,
17956
- category: meta.category,
17957
- agent: meta.agent,
17958
- target_path: meta.target_path ?? undefined,
17959
- outputs: meta.outputs,
17960
- format: meta.format,
17961
- content,
17962
- description: meta.description ?? undefined,
17963
- tags: meta.tags,
17964
- is_template: meta.is_template
17965
- });
17966
- result.created++;
17967
- }
17968
- } catch (err) {
17969
- result.errors.push(`${meta.slug}: ${err instanceof Error ? err.message : String(err)}`);
18465
+ };
18466
+ }
18467
+ async function assertEmptyV2Destination(store) {
18468
+ const [configs, profiles, machines] = await Promise.all([
18469
+ store.listConfigs(),
18470
+ store.listProfiles(),
18471
+ store.listMachines()
18472
+ ]);
18473
+ if (configs.length > 0 || profiles.length > 0 || machines.length > 0) {
18474
+ throw new Error(`Instructions domain archive v2 recovery requires an empty destination ` + `(found configs=${configs.length}, profiles=${profiles.length}, machines=${machines.length})`);
18475
+ }
18476
+ }
18477
+ async function importV2(domain, manifest, store, conflict) {
18478
+ if (conflict !== "skip") {
18479
+ throw new Error(`Instructions domain archive v2 does not support ${conflict}; restore into an empty destination`);
18480
+ }
18481
+ await assertEmptyV2Destination(store);
18482
+ const result = emptyResult();
18483
+ result.integrity = manifest.integrity;
18484
+ const configMap = new Map;
18485
+ for (const archived of domain.configs) {
18486
+ const restored = await restoreNewConfig(store, archived, domain);
18487
+ configMap.set(archived.slug, restored);
18488
+ result.counts.configs.created++;
18489
+ result.created++;
18490
+ result.counts.config_snapshots.created += domain.config_snapshots.filter((row) => row.config_slug === archived.slug).length;
18491
+ }
18492
+ const profileMap = new Map;
18493
+ for (const archived of domain.profiles) {
18494
+ const created = await store.createProfile({
18495
+ name: archived.name,
18496
+ description: archived.description ?? undefined,
18497
+ selectors: archived.selectors,
18498
+ variables: archived.variables
18499
+ });
18500
+ profileMap.set(archived.slug, created);
18501
+ result.counts.profiles.created++;
18502
+ }
18503
+ for (const archived of domain.profiles) {
18504
+ const restoredProfile = profileMap.get(archived.slug);
18505
+ if (!restoredProfile)
18506
+ throw new Error(`post-create profile mapping missing for ${archived.slug}`);
18507
+ 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));
18508
+ 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));
18509
+ for (const row of archivedMembership) {
18510
+ const config = configMap.get(row.config_slug);
18511
+ if (!config)
18512
+ throw new Error(`missing restored config ${row.config_slug}`);
18513
+ await store.addConfigToProfile(restoredProfile.id, config.id);
18514
+ await store.setProfileConfigBinding(restoredProfile.id, config.id, row.binding);
18515
+ result.counts.profile_config_bindings.created++;
18516
+ }
18517
+ for (const row of archivedAssets) {
18518
+ const source = configMap.get(row.source_config_slug);
18519
+ const archivedSource = domain.configs.find((config) => config.slug === row.source_config_slug);
18520
+ if (!source || !archivedSource)
18521
+ throw new Error(`missing restored asset source ${row.source_config_slug}`);
18522
+ await store.addAssetToProfile(restoredProfile.id, source.id, rewriteAssetLocator(row.binding, archivedSource.id, source.id));
18523
+ result.counts.profile_asset_bindings.created++;
18524
+ }
18525
+ }
18526
+ for (const archived of domain.machines) {
18527
+ const restored = await store.registerMachine(archived.hostname, archived.os ?? undefined, archived.arch ?? undefined);
18528
+ if (archived.last_applied_at !== null)
18529
+ await store.updateMachineApplied(restored.hostname);
18530
+ result.counts.machines.created++;
18531
+ }
18532
+ const restoredDomain = await collectInstructionsDomain({ store });
18533
+ const expected = computeRestorableDomainIntegrity(domain);
18534
+ const actual = computeRestorableDomainIntegrity(restoredDomain);
18535
+ if (canonicalDomainJson(actual) !== canonicalDomainJson(expected)) {
18536
+ throw new Error(`Post-restore logical integrity verification failed: expected ${expected.domain_sha256}, got ${actual.domain_sha256}`);
18537
+ }
18538
+ return result;
18539
+ }
18540
+ async function importV1(reader, manifest, store, conflict) {
18541
+ const result = emptyResult();
18542
+ if (!Array.isArray(manifest.configs))
18543
+ throw new Error("Invalid v1 bundle: configs must be an array");
18544
+ for (const meta of manifest.configs) {
18545
+ const ext = meta.format === "text" ? "txt" : meta.format;
18546
+ const contentPath = `contents/${meta.slug}.${ext}`;
18547
+ const content = reader.members.has(contentPath) ? await reader.read(contentPath) : "";
18548
+ const existing = await findConfig(store, meta.slug);
18549
+ if (existing) {
18550
+ if (conflict === "skip") {
18551
+ result.skipped++;
18552
+ result.counts.configs.skipped++;
18553
+ } else {
18554
+ await store.updateConfig(existing.id, {
18555
+ name: meta.name,
18556
+ kind: meta.kind,
18557
+ category: meta.category,
18558
+ agent: meta.agent,
18559
+ target_path: meta.target_path,
18560
+ outputs: meta.outputs,
18561
+ format: meta.format,
18562
+ content,
18563
+ description: meta.description,
18564
+ tags: meta.tags,
18565
+ is_template: meta.is_template,
18566
+ synced_at: meta.synced_at
18567
+ });
18568
+ result.updated++;
18569
+ result.counts.configs.updated++;
17970
18570
  }
17971
- }
17972
- return result;
17973
- } finally {
17974
- if (existsSync19(tmpDir)) {
17975
- rmSync5(tmpDir, { recursive: true, force: true });
18571
+ } else {
18572
+ await store.createConfig(configInput({ ...meta, content }, content));
18573
+ result.created++;
18574
+ result.counts.configs.created++;
17976
18575
  }
17977
18576
  }
18577
+ return result;
18578
+ }
18579
+ async function importConfigs(bundlePath, opts = {}) {
18580
+ const store = opts.store ?? resolveConfigStore();
18581
+ const conflict = opts.conflict ?? "skip";
18582
+ const reader = await openArchive(resolve13(bundlePath));
18583
+ const manifestText = await reader.read("manifest.json");
18584
+ let manifest;
18585
+ try {
18586
+ manifest = JSON.parse(manifestText);
18587
+ } catch {
18588
+ throw new Error("Invalid bundle: manifest.json is not valid JSON");
18589
+ }
18590
+ if (!manifest || typeof manifest !== "object" || typeof manifest.version !== "string") {
18591
+ throw new Error("Invalid bundle: manifest version is missing");
18592
+ }
18593
+ if (manifest.version === "2.0.0") {
18594
+ const v2 = manifest;
18595
+ const domain = await readV2(reader, v2);
18596
+ return importV2(domain, v2, store, conflict);
18597
+ }
18598
+ if (manifest.version.startsWith("1.")) {
18599
+ return importV1(reader, manifest, store, conflict);
18600
+ }
18601
+ throw new Error(`Unsupported Instructions archive version: ${manifest.version}`);
17978
18602
  }
17979
18603
 
17980
18604
  // src/index.ts
@@ -17983,9 +18607,9 @@ init_redact();
17983
18607
 
17984
18608
  // src/lib/package-manager-guard.ts
17985
18609
  import { execFileSync as execFileSync2 } from "child_process";
17986
- import { existsSync as existsSync20, lstatSync as lstatSync7, readdirSync as readdirSync6, readFileSync as readFileSync18 } from "fs";
17987
- import { homedir as homedir11 } from "os";
17988
- import { basename as basename7, dirname as dirname11, isAbsolute as isAbsolute7, join as join22, relative as relative7, resolve as resolve14 } from "path";
18610
+ import { existsSync as existsSync19, lstatSync as lstatSync7, readdirSync as readdirSync6, readFileSync as readFileSync17 } from "fs";
18611
+ import { homedir as homedir10 } from "os";
18612
+ import { basename as basename7, dirname as dirname12, isAbsolute as isAbsolute8, join as join21, relative as relative8, resolve as resolve14 } from "path";
17989
18613
  var SKIP_DIRS = new Set([
17990
18614
  ".git",
17991
18615
  "node_modules",
@@ -18027,7 +18651,7 @@ function scanPackageManagerSecrets(options = {}) {
18027
18651
  const findings = [];
18028
18652
  let scannedFiles = 0;
18029
18653
  for (const root of roots) {
18030
- if (!existsSync20(root))
18654
+ if (!existsSync19(root))
18031
18655
  continue;
18032
18656
  const stat = lstatSync7(root);
18033
18657
  if (stat.isFile()) {
@@ -18037,14 +18661,14 @@ function scanPackageManagerSecrets(options = {}) {
18037
18661
  if (text === null)
18038
18662
  continue;
18039
18663
  scannedFiles++;
18040
- findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname11(root)));
18664
+ findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname12(root)));
18041
18665
  continue;
18042
18666
  }
18043
18667
  if (!stat.isDirectory())
18044
18668
  continue;
18045
18669
  const tracked = trackedFiles(root);
18046
18670
  for (const file of collectRepoFiles(root)) {
18047
- const rel = toPosix(relative7(root, file));
18671
+ const rel = toPosix(relative8(root, file));
18048
18672
  const isTracked = tracked.has(rel);
18049
18673
  const text = readTextFile(file);
18050
18674
  if (text === null)
@@ -18054,10 +18678,10 @@ function scanPackageManagerSecrets(options = {}) {
18054
18678
  }
18055
18679
  }
18056
18680
  if (options.includeHome) {
18057
- const home = homedir11();
18681
+ const home = homedir10();
18058
18682
  for (const name of HOME_FILES) {
18059
- const file = join22(home, name);
18060
- if (!existsSync20(file))
18683
+ const file = join21(home, name);
18684
+ if (!existsSync19(file))
18061
18685
  continue;
18062
18686
  const text = readTextFile(file);
18063
18687
  if (text === null)
@@ -18081,12 +18705,12 @@ function collectRepoFiles(root) {
18081
18705
  if (entry.isDirectory()) {
18082
18706
  if (SKIP_DIRS.has(entry.name))
18083
18707
  continue;
18084
- visit(join22(dir, entry.name));
18708
+ visit(join21(dir, entry.name));
18085
18709
  continue;
18086
18710
  }
18087
18711
  if (!entry.isFile())
18088
18712
  continue;
18089
- const file = join22(dir, entry.name);
18713
+ const file = join21(dir, entry.name);
18090
18714
  if (shouldScanRepoFile(file))
18091
18715
  out.push(file);
18092
18716
  }
@@ -18124,7 +18748,7 @@ function readTextFile(file) {
18124
18748
  const stat = lstatSync7(file);
18125
18749
  if (!stat.isFile() || stat.size > 5000000)
18126
18750
  return null;
18127
- const buf = readFileSync18(file);
18751
+ const buf = readFileSync17(file);
18128
18752
  if (buf.includes(0))
18129
18753
  return null;
18130
18754
  return buf.toString("utf-8");
@@ -18324,11 +18948,11 @@ function trackedFiles(root) {
18324
18948
  }
18325
18949
  function isTrackedFile(file) {
18326
18950
  try {
18327
- const repoRoot = execFileSync2("git", ["-C", dirname11(file), "rev-parse", "--show-toplevel"], {
18951
+ const repoRoot = execFileSync2("git", ["-C", dirname12(file), "rev-parse", "--show-toplevel"], {
18328
18952
  encoding: "utf-8",
18329
18953
  stdio: ["ignore", "pipe", "ignore"]
18330
18954
  }).trim();
18331
- const rel = toPosix(relative7(repoRoot, file));
18955
+ const rel = toPosix(relative8(repoRoot, file));
18332
18956
  execFileSync2("git", ["-C", repoRoot, "ls-files", "--error-unmatch", "--", rel], {
18333
18957
  stdio: ["ignore", "ignore", "ignore"]
18334
18958
  });
@@ -18354,13 +18978,13 @@ function stripInlineComment(value) {
18354
18978
  return value.replace(/\s[#;].*$/, "").trim();
18355
18979
  }
18356
18980
  function displayPath(file, root) {
18357
- const home = homedir11();
18981
+ const home = homedir10();
18358
18982
  if (root === home && (file === home || file.startsWith(home + "/")))
18359
- return "~/" + toPosix(relative7(home, file));
18360
- if (isAbsolute7(root) && file.startsWith(root + "/"))
18361
- return toPosix(relative7(root, file));
18983
+ return "~/" + toPosix(relative8(home, file));
18984
+ if (isAbsolute8(root) && file.startsWith(root + "/"))
18985
+ return toPosix(relative8(root, file));
18362
18986
  if (file === home || file.startsWith(home + "/"))
18363
- return "~/" + toPosix(relative7(home, file));
18987
+ return "~/" + toPosix(relative8(home, file));
18364
18988
  return file;
18365
18989
  }
18366
18990
  function toPosix(path) {
@@ -18526,6 +19150,7 @@ export {
18526
19150
  INSTRUCTION_ACTIVATION_MODES,
18527
19151
  INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS,
18528
19152
  INSTRUCTIONS_LOCAL_OPT_IN_ENV,
19153
+ INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
18529
19154
  INBOX_CONVERSATIONS_MINIMUM_VERSION,
18530
19155
  GLOBAL_AGENT_RULES_STANDARD_SLUG,
18531
19156
  GLOBAL_AGENT_RULES_STANDARD_CONTENT,