@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.
Files changed (85) hide show
  1. package/README.md +141 -10
  2. package/dist/chunks/{apply-ynfn1av7.js → apply-c3sd4agz.js} +5 -3
  3. package/dist/chunks/{apply-sfzja1f3.js → apply-pnn7a3y7.js} +6 -3
  4. package/dist/chunks/database-wstmr56z.js +23 -0
  5. package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
  6. package/dist/chunks/index-5d7zdftc.js +186 -0
  7. package/dist/chunks/{index-1hpwskwd.js → index-6jncnnw7.js} +318 -329
  8. package/dist/chunks/index-8rb7ng2y.js +148 -0
  9. package/dist/chunks/{index-v7dkb384.js → index-aj66f6xr.js} +243 -100
  10. package/dist/chunks/index-fpeyzn3n.js +186 -0
  11. package/dist/chunks/{index-9p9gn7gh.js → index-jg8ptcwh.js} +5 -7
  12. package/dist/chunks/index-mvckkf15.js +363 -0
  13. package/dist/chunks/index-n3fjspr0.js +119 -0
  14. package/dist/chunks/{index-85n5wnd8.js → index-pdpg8a44.js} +5 -7
  15. package/dist/chunks/{index-e9epbsvs.js → index-r7nfef2g.js} +15 -4
  16. package/dist/chunks/{index-axhwg61p.js → index-ypcef3a2.js} +15 -4
  17. package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
  18. package/dist/chunks/{local-a9nen65z.js → local-0a9gja58.js} +142 -2
  19. package/dist/chunks/{local-0d8qrc92.js → local-969ye538.js} +153 -188
  20. package/dist/chunks/s3-backup-hxe3zpfz.js +240 -0
  21. package/dist/chunks/s3-config-hhght91k.js +14 -0
  22. package/dist/chunks/s3-object-store-n0s82swt.js +14 -0
  23. package/dist/chunks/{sync-vy1yjvs6.js → sync-q5hvwq6e.js} +4 -4
  24. package/dist/chunks/{sync-m46fc257.js → sync-xtkbm5br.js} +5 -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 +1236 -256
  28. package/dist/data/config-store.d.ts +10 -1
  29. package/dist/data/config-store.d.ts.map +1 -1
  30. package/dist/db/configs.d.ts +4 -1
  31. package/dist/db/configs.d.ts.map +1 -1
  32. package/dist/db/local.d.ts +1 -1
  33. package/dist/db/local.d.ts.map +1 -1
  34. package/dist/index.js +1086 -215
  35. package/dist/lib/apply.d.ts +7 -0
  36. package/dist/lib/apply.d.ts.map +1 -1
  37. package/dist/lib/compact-output.d.ts +3 -18
  38. package/dist/lib/compact-output.d.ts.map +1 -1
  39. package/dist/lib/export.d.ts +22 -3
  40. package/dist/lib/export.d.ts.map +1 -1
  41. package/dist/lib/import.d.ts +18 -0
  42. package/dist/lib/import.d.ts.map +1 -1
  43. package/dist/lib/legacy-store-migration.d.ts +39 -0
  44. package/dist/lib/legacy-store-migration.d.ts.map +1 -0
  45. package/dist/lib/managed-skill-runtimes.d.ts +1 -0
  46. package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
  47. package/dist/lib/sync-dir.d.ts.map +1 -1
  48. package/dist/mcp/index.js +41 -30
  49. package/dist/mcp/server.d.ts.map +1 -1
  50. package/dist/sdk/index.d.ts +2 -0
  51. package/dist/sdk/index.d.ts.map +1 -1
  52. package/dist/sdk/index.js +229 -10
  53. package/dist/sdk/resolve.d.ts +1 -1
  54. package/dist/sdk/resolve.d.ts.map +1 -1
  55. package/dist/sdk/v1-client.d.ts +58 -0
  56. package/dist/sdk/v1-client.d.ts.map +1 -0
  57. package/dist/sdk/v1.generated.d.ts +242 -21
  58. package/dist/sdk/v1.generated.d.ts.map +1 -1
  59. package/dist/server/cloud.d.ts.map +1 -1
  60. package/dist/server/index.d.ts +13 -5
  61. package/dist/server/index.d.ts.map +1 -1
  62. package/dist/server/index.js +1261 -427
  63. package/dist/server/openapi.d.ts +1379 -93
  64. package/dist/server/openapi.d.ts.map +1 -1
  65. package/dist/server/v1.d.ts +25 -1
  66. package/dist/server/v1.d.ts.map +1 -1
  67. package/dist/storage/cloud-store.d.ts +45 -2
  68. package/dist/storage/cloud-store.d.ts.map +1 -1
  69. package/dist/storage/index.d.ts +7 -0
  70. package/dist/storage/index.d.ts.map +1 -0
  71. package/dist/storage/index.js +744 -0
  72. package/dist/storage/s3-backup.d.ts +76 -0
  73. package/dist/storage/s3-backup.d.ts.map +1 -0
  74. package/dist/storage/s3-config.d.ts +46 -0
  75. package/dist/storage/s3-config.d.ts.map +1 -0
  76. package/dist/storage/s3-object-store.d.ts +70 -0
  77. package/dist/storage/s3-object-store.d.ts.map +1 -0
  78. package/dist/types/index.d.ts +98 -3
  79. package/dist/types/index.d.ts.map +1 -1
  80. package/hasna.contract.json +110 -0
  81. package/migrations/0001_instructions.sql +63 -0
  82. package/migrations/0002_api_keys.sql +20 -0
  83. package/migrations/0003_idempotency_receipts.sql +24 -0
  84. package/package.json +24 -13
  85. package/assets/skills/inbox/SKILL.md +0 -86
package/dist/cli/index.js CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- import {
4
- slugify
5
- } from "../chunks/index-ma2xmp6c.js";
6
3
  import {
7
4
  KNOWN_CONFIGS,
8
5
  detectAgent,
@@ -12,12 +9,13 @@ import {
12
9
  syncKnown,
13
10
  syncProject,
14
11
  syncToDisk
15
- } from "../chunks/index-85n5wnd8.js";
12
+ } from "../chunks/index-pdpg8a44.js";
16
13
  import {
17
14
  applyConfigsWithReport,
15
+ compactPathForConfigHome,
18
16
  expandPath,
19
17
  normalizeTargetPath
20
- } from "../chunks/index-e9epbsvs.js";
18
+ } from "../chunks/index-r7nfef2g.js";
21
19
  import {
22
20
  CloudConfigStore,
23
21
  DEFAULT_LIST_LIMIT,
@@ -35,7 +33,6 @@ import {
35
33
  ensureCodewithSharedTodosStorageStandardConfig,
36
34
  ensureGlobalAgentRulesStandardConfig,
37
35
  formatCliError,
38
- getRawStoreRoot,
39
36
  getSessionRenderSnapshotDir,
40
37
  isRetiredOrUnsupportedConfigAgent,
41
38
  normalizeProfileAssetBinding,
@@ -64,10 +61,18 @@ import {
64
61
  truncateText,
65
62
  withProjectContextSessionGuard,
66
63
  writeProjectContextCoordinatedFile
67
- } from "../chunks/index-1hpwskwd.js";
64
+ } from "../chunks/index-6jncnnw7.js";
68
65
  import {
66
+ INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
69
67
  extractTemplateVars
70
- } from "../chunks/index-pkscjnj5.js";
68
+ } from "../chunks/index-n3fjspr0.js";
69
+ import {
70
+ slugify
71
+ } from "../chunks/index-ma2xmp6c.js";
72
+ import {
73
+ getRawStoreRoot,
74
+ selectsInstructionsLocalStore
75
+ } from "../chunks/index-fpeyzn3n.js";
71
76
  import {
72
77
  __commonJS,
73
78
  __require,
@@ -3224,11 +3229,11 @@ function setPath(input, path, replacement) {
3224
3229
  if (last && last in cursor)
3225
3230
  cursor[last] = replacement;
3226
3231
  }
3232
+ var MAX_PACKAGE_BYTES = 1024 * 1024;
3227
3233
  var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
3228
3234
  var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
3229
3235
  var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
3230
3236
  var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
3231
- var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
3232
3237
  var requireSecretsSdk = createRequire(import.meta.url);
3233
3238
  var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
3234
3239
  var AUTHORITY_OVERRIDE_HEADERS = new Set([
@@ -3779,9 +3784,9 @@ var {
3779
3784
 
3780
3785
  // src/cli/index.tsx
3781
3786
  import chalk from "chalk";
3782
- import { existsSync as existsSync12, lstatSync as lstatSync4, readFileSync as readFileSync8, readSync, writeSync } from "fs";
3787
+ import { existsSync as existsSync11, lstatSync as lstatSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync7, readSync, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync5, writeSync } from "fs";
3783
3788
  import { homedir as homedir4 } from "os";
3784
- import { basename, join as join11, resolve as resolve6 } from "path";
3789
+ import { basename, dirname as dirname6, join as join10, resolve as resolve6 } from "path";
3785
3790
 
3786
3791
  // src/lib/config-target-identity.ts
3787
3792
  function findConfigsByTargetPath(configs, targetPath) {
@@ -3820,124 +3825,690 @@ function findDuplicateTargetPathGroups(configs) {
3820
3825
  }
3821
3826
 
3822
3827
  // src/lib/export.ts
3823
- import { existsSync as existsSync3, mkdirSync, rmSync, writeFileSync } from "fs";
3824
- import { join as join3, resolve as resolve2 } from "path";
3828
+ import { createHash } from "crypto";
3829
+ import { existsSync as existsSync3, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
3830
+ import { dirname, join as join3, resolve as resolve2 } from "path";
3825
3831
  import { tmpdir } from "os";
3826
- async function exportConfigs(outputPath, opts = {}) {
3832
+ var ARCHIVE_EXCLUSIONS = [
3833
+ {
3834
+ entity: "api_keys",
3835
+ classification: "security_state",
3836
+ reason: "API credentials are security state and must be provisioned independently, never copied in a domain archive."
3837
+ },
3838
+ {
3839
+ entity: "idempotency_receipts",
3840
+ classification: "transport_state",
3841
+ reason: "Request replay receipts are transport state and are not portable business-domain data."
3842
+ },
3843
+ {
3844
+ entity: "feedback",
3845
+ classification: "out_of_domain",
3846
+ reason: "Feedback is product telemetry, not part of the Instructions configuration domain."
3847
+ }
3848
+ ];
3849
+ function compareText(left, right) {
3850
+ return left < right ? -1 : left > right ? 1 : 0;
3851
+ }
3852
+ function sha256(value) {
3853
+ return createHash("sha256").update(value).digest("hex");
3854
+ }
3855
+ function canonicalize(value) {
3856
+ if (Array.isArray(value))
3857
+ return value.map(canonicalize);
3858
+ if (value && typeof value === "object") {
3859
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => compareText(left, right)).map(([key, entry]) => [key, canonicalize(entry)]));
3860
+ }
3861
+ return value;
3862
+ }
3863
+ function canonicalDomainJson(value) {
3864
+ return JSON.stringify(canonicalize(value));
3865
+ }
3866
+ function logicalAssetLocator(locator, idToSlug) {
3867
+ const match = /^config:\/\/([^@]+)@(\d+)$/.exec(locator);
3868
+ if (!match)
3869
+ return locator;
3870
+ let configId;
3871
+ try {
3872
+ configId = decodeURIComponent(match[1]);
3873
+ } catch {
3874
+ return locator;
3875
+ }
3876
+ const slug = idToSlug.get(configId);
3877
+ return slug ? `config-slug://${encodeURIComponent(slug)}@${match[2]}` : locator;
3878
+ }
3879
+ function logicalCollections(domain, options) {
3880
+ const configs = [...domain.configs].sort((left, right) => compareText(left.slug, right.slug));
3881
+ const snapshots = domain.config_snapshots.map((snapshot) => ({
3882
+ config_slug: snapshot.config_slug,
3883
+ version: snapshot.version,
3884
+ content: snapshot.content,
3885
+ ...options.includeOperationalTimestamps ? { created_at: snapshot.created_at } : {}
3886
+ })).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 ?? "" : ""));
3887
+ const profiles = [...domain.profiles].sort((left, right) => compareText(left.slug, right.slug));
3888
+ 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));
3889
+ 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));
3890
+ const machines = [...domain.machines].sort((left, right) => compareText(left.hostname, right.hostname));
3891
+ const idToSlug = new Map(configs.map((config) => [config.id, config.slug]));
3892
+ const configOrder = new Map;
3893
+ const assetOrder = new Map;
3894
+ return {
3895
+ configs: configs.map((config) => ({
3896
+ slug: config.slug,
3897
+ name: config.name,
3898
+ kind: config.kind,
3899
+ category: config.category,
3900
+ agent: config.agent,
3901
+ target_path: config.target_path,
3902
+ outputs: config.outputs,
3903
+ format: config.format,
3904
+ content: config.content,
3905
+ description: config.description,
3906
+ tags: [...config.tags].sort(compareText),
3907
+ is_template: config.is_template,
3908
+ version: config.version,
3909
+ ...options.includeOperationalTimestamps ? {
3910
+ created_at: config.created_at,
3911
+ updated_at: config.updated_at,
3912
+ synced_at: config.synced_at
3913
+ } : {}
3914
+ })),
3915
+ config_snapshots: snapshots,
3916
+ profiles: profiles.map((profile) => ({
3917
+ slug: profile.slug,
3918
+ name: profile.name,
3919
+ description: profile.description,
3920
+ selectors: profile.selectors,
3921
+ variables: profile.variables,
3922
+ ...options.includeOperationalTimestamps ? {
3923
+ created_at: profile.created_at,
3924
+ updated_at: profile.updated_at
3925
+ } : {}
3926
+ })),
3927
+ profile_config_bindings: configBindings.map((row) => {
3928
+ const order = configOrder.get(row.profile_slug) ?? 0;
3929
+ configOrder.set(row.profile_slug, order + 1);
3930
+ return {
3931
+ profile_slug: row.profile_slug,
3932
+ config_slug: row.config_slug,
3933
+ order,
3934
+ binding: row.binding
3935
+ };
3936
+ }),
3937
+ profile_asset_bindings: assetBindings.map((row) => {
3938
+ const order = assetOrder.get(row.profile_slug) ?? 0;
3939
+ assetOrder.set(row.profile_slug, order + 1);
3940
+ return {
3941
+ profile_slug: row.profile_slug,
3942
+ source_config_slug: row.source_config_slug,
3943
+ order,
3944
+ binding: {
3945
+ ...row.binding,
3946
+ source: {
3947
+ ...row.binding.source,
3948
+ locator: logicalAssetLocator(row.binding.source.locator, idToSlug)
3949
+ }
3950
+ }
3951
+ };
3952
+ }),
3953
+ machines: machines.map((machine) => ({
3954
+ hostname: machine.hostname,
3955
+ os: machine.os,
3956
+ arch: machine.arch,
3957
+ ...options.includeOperationalTimestamps ? {
3958
+ created_at: machine.created_at,
3959
+ last_applied_at: machine.last_applied_at
3960
+ } : {
3961
+ applied: machine.last_applied_at !== null
3962
+ }
3963
+ }))
3964
+ };
3965
+ }
3966
+ function computeIntegrity(domain, options) {
3967
+ const collections = logicalCollections(domain, options);
3968
+ const counts = {
3969
+ configs: collections.configs.length,
3970
+ config_snapshots: collections.config_snapshots.length,
3971
+ profiles: collections.profiles.length,
3972
+ profile_config_bindings: collections.profile_config_bindings.length,
3973
+ profile_asset_bindings: collections.profile_asset_bindings.length,
3974
+ machines: collections.machines.length
3975
+ };
3976
+ const hashes = {
3977
+ configs: sha256(canonicalDomainJson(collections.configs)),
3978
+ config_snapshots: sha256(canonicalDomainJson(collections.config_snapshots)),
3979
+ profiles: sha256(canonicalDomainJson(collections.profiles)),
3980
+ profile_config_bindings: sha256(canonicalDomainJson(collections.profile_config_bindings)),
3981
+ profile_asset_bindings: sha256(canonicalDomainJson(collections.profile_asset_bindings)),
3982
+ machines: sha256(canonicalDomainJson(collections.machines))
3983
+ };
3984
+ return {
3985
+ algorithm: "sha256",
3986
+ canonicalization: options.canonicalization,
3987
+ counts,
3988
+ hashes,
3989
+ domain_sha256: sha256(canonicalDomainJson({ counts, hashes }))
3990
+ };
3991
+ }
3992
+ function computeDomainIntegrity(domain) {
3993
+ return computeIntegrity(domain, {
3994
+ includeOperationalTimestamps: true,
3995
+ canonicalization: "hasna.instructions.logical-json/v1"
3996
+ });
3997
+ }
3998
+ function computeRestorableDomainIntegrity(domain) {
3999
+ return computeIntegrity(domain, {
4000
+ includeOperationalTimestamps: false,
4001
+ canonicalization: "hasna.instructions.restorable-logical-json/v1"
4002
+ });
4003
+ }
4004
+ function assertArray(value, label) {
4005
+ if (!Array.isArray(value))
4006
+ throw new Error(`Invalid v2 archive: ${label} must be an array`);
4007
+ }
4008
+ function assertUnique(values, label) {
4009
+ const seen = new Set;
4010
+ for (const value of values) {
4011
+ if (!value || seen.has(value))
4012
+ throw new Error(`Invalid v2 archive: duplicate or empty ${label} ${JSON.stringify(value)}`);
4013
+ seen.add(value);
4014
+ }
4015
+ }
4016
+ function assertTimestamp(value, label, nullable = false) {
4017
+ if (nullable && value === null)
4018
+ return;
4019
+ if (typeof value !== "string" || value.length === 0 || Number.isNaN(Date.parse(value))) {
4020
+ throw new Error(`Invalid v2 archive: ${label} must be ${nullable ? "null or " : ""}an ISO timestamp`);
4021
+ }
4022
+ }
4023
+ function validateInstructionsDomainArchive(value) {
4024
+ if (!value || typeof value !== "object")
4025
+ throw new Error("Invalid v2 archive: domain payload must be an object");
4026
+ const domain = value;
4027
+ if (domain.schema !== INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA)
4028
+ throw new Error("Invalid v2 archive: unsupported domain schema");
4029
+ assertArray(domain.configs, "configs");
4030
+ assertArray(domain.config_snapshots, "config_snapshots");
4031
+ assertArray(domain.profiles, "profiles");
4032
+ assertArray(domain.profile_config_bindings, "profile_config_bindings");
4033
+ assertArray(domain.profile_asset_bindings, "profile_asset_bindings");
4034
+ assertArray(domain.machines, "machines");
4035
+ const typed = domain;
4036
+ assertUnique(typed.configs.map((row) => row.slug), "config slug");
4037
+ assertUnique(typed.profiles.map((row) => row.slug), "profile slug");
4038
+ assertUnique(typed.machines.map((row) => row.hostname), "machine hostname");
4039
+ assertUnique(typed.config_snapshots.map((row) => row.id), "config snapshot id");
4040
+ assertUnique(typed.profile_config_bindings.map((row) => `${row.profile_slug}:${row.config_slug}`), "profile config binding");
4041
+ assertUnique(typed.profile_asset_bindings.map((row) => `${row.profile_slug}:${row.binding.assetKey}`), "profile asset binding");
4042
+ const configsBySlug = new Map(typed.configs.map((row) => [row.slug, row]));
4043
+ const configSlugs = new Set(configsBySlug.keys());
4044
+ const profileSlugs = new Set(typed.profiles.map((row) => row.slug));
4045
+ for (const config of typed.configs) {
4046
+ assertTimestamp(config.created_at, `config ${config.slug} created_at`);
4047
+ assertTimestamp(config.updated_at, `config ${config.slug} updated_at`);
4048
+ assertTimestamp(config.synced_at, `config ${config.slug} synced_at`, true);
4049
+ if (!Number.isSafeInteger(config.version) || config.version < 1)
4050
+ throw new Error(`Invalid v2 archive: config ${config.slug} has invalid version`);
4051
+ }
4052
+ for (const row of typed.config_snapshots) {
4053
+ const config = configsBySlug.get(row.config_slug);
4054
+ if (!config) {
4055
+ throw new Error(`Invalid v2 archive: snapshot references missing config ${row.config_slug}`);
4056
+ }
4057
+ if (!Number.isSafeInteger(row.version) || row.version < 1 || row.version > config.version) {
4058
+ throw new Error(`Invalid v2 archive: config ${row.config_slug} has an invalid snapshot version`);
4059
+ }
4060
+ assertTimestamp(row.created_at, `snapshot ${row.config_slug}@${row.version} created_at`);
4061
+ }
4062
+ for (const profile of typed.profiles) {
4063
+ assertTimestamp(profile.created_at, `profile ${profile.slug} created_at`);
4064
+ assertTimestamp(profile.updated_at, `profile ${profile.slug} updated_at`);
4065
+ }
4066
+ for (const row of typed.profile_config_bindings) {
4067
+ if (!profileSlugs.has(row.profile_slug) || !configSlugs.has(row.config_slug)) {
4068
+ throw new Error(`Invalid v2 archive: profile config binding has a missing logical reference`);
4069
+ }
4070
+ }
4071
+ for (const row of typed.profile_asset_bindings) {
4072
+ if (!profileSlugs.has(row.profile_slug) || !configSlugs.has(row.source_config_slug)) {
4073
+ throw new Error(`Invalid v2 archive: profile asset binding has a missing logical reference`);
4074
+ }
4075
+ }
4076
+ for (const machine of typed.machines) {
4077
+ assertTimestamp(machine.created_at, `machine ${machine.hostname} created_at`);
4078
+ assertTimestamp(machine.last_applied_at, `machine ${machine.hostname} last_applied_at`, true);
4079
+ }
4080
+ return typed;
4081
+ }
4082
+ function matchesFilter(config, filter) {
4083
+ if (!filter)
4084
+ return true;
4085
+ if (filter.category && config.category !== filter.category)
4086
+ return false;
4087
+ if (filter.agent && config.agent !== filter.agent)
4088
+ return false;
4089
+ if (filter.kind && config.kind !== filter.kind)
4090
+ return false;
4091
+ if (filter.is_template !== undefined && config.is_template !== filter.is_template)
4092
+ return false;
4093
+ if (filter.tags?.length && !filter.tags.every((tag) => config.tags.includes(tag)))
4094
+ return false;
4095
+ if (filter.search) {
4096
+ const needle = filter.search.toLocaleLowerCase();
4097
+ if (![config.name, config.description ?? "", config.content].some((value) => value.toLocaleLowerCase().includes(needle)))
4098
+ return false;
4099
+ }
4100
+ return true;
4101
+ }
4102
+ async function collectInstructionsDomain(opts = {}) {
3827
4103
  const store = opts.store ?? resolveConfigStore();
3828
- const configs = await store.listConfigs(opts.filter);
4104
+ const selectedProfile = opts.profileId ? await store.getProfile(opts.profileId) : null;
4105
+ const listedConfigs = selectedProfile ? await store.getProfileConfigs(selectedProfile.id) : await store.listConfigs(opts.filter);
4106
+ const configs = listedConfigs.filter((config) => matchesFilter(config, selectedProfile ? opts.filter : undefined));
4107
+ configs.sort((left, right) => compareText(left.slug, right.slug));
4108
+ const selectedIds = new Set(configs.map((config) => config.id));
4109
+ const profiles = (selectedProfile ? [selectedProfile] : await store.listProfiles()).sort((left, right) => compareText(left.slug, right.slug));
4110
+ const profileById = new Map(profiles.map((profile) => [profile.id, profile]));
4111
+ const configById = new Map(configs.map((config) => [config.id, config]));
4112
+ const configSnapshots = [];
4113
+ for (const config of configs) {
4114
+ for (const snapshot of await store.listSnapshots(config.id)) {
4115
+ configSnapshots.push({
4116
+ id: snapshot.id,
4117
+ config_slug: config.slug,
4118
+ content: snapshot.content,
4119
+ version: snapshot.version,
4120
+ created_at: snapshot.created_at
4121
+ });
4122
+ }
4123
+ }
4124
+ configSnapshots.sort((left, right) => compareText(left.config_slug, right.config_slug) || left.version - right.version || compareText(left.id, right.id));
4125
+ const profileConfigBindings = (await Promise.all(profiles.map(async (profile) => (await store.getProfileConfigBindings(profile.id)).filter((row) => selectedIds.has(row.config_id)).map((row) => ({
4126
+ profile_slug: profile.slug,
4127
+ config_slug: configById.get(row.config_id).slug,
4128
+ sort_order: row.sort_order,
4129
+ binding: row.binding
4130
+ }))))).flat().sort((left, right) => compareText(left.profile_slug, right.profile_slug) || left.sort_order - right.sort_order || compareText(left.config_slug, right.config_slug));
4131
+ const profileAssetBindings = (await Promise.all(profiles.map(async (profile) => (await store.getProfileAssetBindings(profile.id)).filter((row) => selectedIds.has(row.source_config_id)).map((row) => ({
4132
+ profile_slug: profile.slug,
4133
+ source_config_slug: configById.get(row.source_config_id).slug,
4134
+ sort_order: row.sort_order,
4135
+ binding: row.binding
4136
+ }))))).flat().sort((left, right) => compareText(left.profile_slug, right.profile_slug) || left.sort_order - right.sort_order || compareText(left.binding.assetKey, right.binding.assetKey));
4137
+ const archivedProfiles = profiles.filter((profile) => profileById.has(profile.id));
4138
+ const machines = (await store.listMachines()).sort((left, right) => compareText(left.hostname, right.hostname));
4139
+ return {
4140
+ schema: INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
4141
+ configs,
4142
+ config_snapshots: configSnapshots,
4143
+ profiles: archivedProfiles,
4144
+ profile_config_bindings: profileConfigBindings,
4145
+ profile_asset_bindings: profileAssetBindings,
4146
+ machines
4147
+ };
4148
+ }
4149
+ async function exportConfigs(outputPath, opts = {}) {
4150
+ const domain = validateInstructionsDomainArchive(await collectInstructionsDomain(opts));
4151
+ const integrity = computeDomainIntegrity(domain);
4152
+ const payload = `${JSON.stringify(domain, null, 2)}
4153
+ `;
4154
+ const manifest = {
4155
+ schema: INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
4156
+ version: "2.0.0",
4157
+ exported_at: new Date().toISOString(),
4158
+ payload: {
4159
+ path: "domain.json",
4160
+ sha256: sha256(payload),
4161
+ size_bytes: Buffer.byteLength(payload)
4162
+ },
4163
+ integrity,
4164
+ exclusions: ARCHIVE_EXCLUSIONS
4165
+ };
3829
4166
  const absOutput = resolve2(outputPath);
3830
- const tmpDir = join3(tmpdir(), `configs-export-${Date.now()}`);
3831
- const contentsDir = join3(tmpDir, "contents");
4167
+ mkdirSync(dirname(absOutput), { recursive: true });
4168
+ const stagingDir = mkdtempSync(join3(tmpdir(), "instructions-domain-export-"));
3832
4169
  try {
3833
- mkdirSync(contentsDir, { recursive: true });
3834
- const manifest = {
3835
- version: "1.0.0",
3836
- exported_at: new Date().toISOString(),
3837
- configs: configs.map(({ content: _content, ...meta }) => meta)
3838
- };
3839
- writeFileSync(join3(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
3840
- for (const config of configs) {
3841
- const fileName = `${config.slug}.${config.format === "text" ? "txt" : config.format}`;
3842
- writeFileSync(join3(contentsDir, fileName), config.content, "utf-8");
3843
- }
3844
- const proc = Bun.spawn(["tar", "czf", absOutput, "-C", tmpDir, "."], {
4170
+ writeFileSync(join3(stagingDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
4171
+ `, "utf-8");
4172
+ writeFileSync(join3(stagingDir, "domain.json"), payload, "utf-8");
4173
+ const proc = Bun.spawn(["tar", "czf", absOutput, "-C", stagingDir, "manifest.json", "domain.json"], {
3845
4174
  stdout: "pipe",
3846
4175
  stderr: "pipe"
3847
4176
  });
3848
4177
  const exitCode = await proc.exited;
3849
4178
  if (exitCode !== 0) {
3850
4179
  const stderr = await new Response(proc.stderr).text();
3851
- throw new Error(`tar failed: ${stderr}`);
4180
+ throw new Error(`tar failed: ${stderr.trim()}`);
3852
4181
  }
3853
- return { path: absOutput, count: configs.length };
4182
+ return { path: absOutput, count: domain.configs.length, counts: integrity.counts, integrity };
3854
4183
  } finally {
3855
- if (existsSync3(tmpDir)) {
3856
- rmSync(tmpDir, { recursive: true, force: true });
3857
- }
4184
+ if (existsSync3(stagingDir))
4185
+ rmSync(stagingDir, { recursive: true, force: true });
3858
4186
  }
3859
4187
  }
3860
4188
 
3861
4189
  // src/lib/import.ts
3862
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync, rmSync as rmSync2 } from "fs";
3863
- import { join as join5, resolve as resolve3 } from "path";
3864
- import { tmpdir as tmpdir2 } from "os";
3865
- async function importConfigs(bundlePath, opts = {}) {
3866
- const store = opts.store ?? resolveConfigStore();
3867
- const conflict = opts.conflict ?? "skip";
3868
- const absPath = resolve3(bundlePath);
3869
- const tmpDir = join5(tmpdir2(), `configs-import-${Date.now()}`);
3870
- const result = { created: 0, updated: 0, skipped: 0, errors: [] };
4190
+ import { createHash as createHash2 } from "crypto";
4191
+ import { resolve as resolve3 } from "path";
4192
+ function compareText2(left, right) {
4193
+ return left < right ? -1 : left > right ? 1 : 0;
4194
+ }
4195
+ function emptyMutationCounts() {
4196
+ return { created: 0, updated: 0, skipped: 0 };
4197
+ }
4198
+ function emptyResult() {
4199
+ return {
4200
+ created: 0,
4201
+ updated: 0,
4202
+ skipped: 0,
4203
+ counts: {
4204
+ configs: emptyMutationCounts(),
4205
+ config_snapshots: { created: 0, skipped: 0 },
4206
+ profiles: emptyMutationCounts(),
4207
+ profile_config_bindings: emptyMutationCounts(),
4208
+ profile_asset_bindings: emptyMutationCounts(),
4209
+ machines: emptyMutationCounts()
4210
+ },
4211
+ errors: [],
4212
+ integrity: null
4213
+ };
4214
+ }
4215
+ function sha2562(value) {
4216
+ return createHash2("sha256").update(value).digest("hex");
4217
+ }
4218
+ function normalizeMemberName(name) {
4219
+ let normalized = name;
4220
+ while (normalized.startsWith("./"))
4221
+ normalized = normalized.slice(2);
4222
+ if (normalized.startsWith("/") || normalized.includes("\x00") || normalized.split("/").some((segment) => segment === "..")) {
4223
+ throw new Error(`Invalid archive member path: ${JSON.stringify(name)}`);
4224
+ }
4225
+ return normalized;
4226
+ }
4227
+ async function openArchive(path) {
4228
+ const list = Bun.spawn(["tar", "tzf", path], { stdout: "pipe", stderr: "pipe" });
4229
+ const [exitCode, stdout, stderr] = await Promise.all([
4230
+ list.exited,
4231
+ new Response(list.stdout).text(),
4232
+ new Response(list.stderr).text()
4233
+ ]);
4234
+ if (exitCode !== 0)
4235
+ throw new Error(`tar listing failed: ${stderr.trim()}`);
4236
+ const members = new Map;
4237
+ for (const raw of stdout.split(`
4238
+ `).filter(Boolean)) {
4239
+ const normalized = normalizeMemberName(raw);
4240
+ if (normalized.length === 0)
4241
+ continue;
4242
+ if (members.has(normalized))
4243
+ throw new Error(`Invalid archive: duplicate member ${normalized}`);
4244
+ members.set(normalized, raw);
4245
+ }
4246
+ return {
4247
+ members,
4248
+ async read(member) {
4249
+ const actual = members.get(member);
4250
+ if (!actual)
4251
+ throw new Error(`Invalid archive: missing ${member}`);
4252
+ const proc = Bun.spawn(["tar", "xOzf", path, "--", actual], { stdout: "pipe", stderr: "pipe" });
4253
+ const [code, content, memberError] = await Promise.all([
4254
+ proc.exited,
4255
+ new Response(proc.stdout).text(),
4256
+ new Response(proc.stderr).text()
4257
+ ]);
4258
+ if (code !== 0)
4259
+ throw new Error(`tar read failed for ${member}: ${memberError.trim()}`);
4260
+ return content;
4261
+ }
4262
+ };
4263
+ }
4264
+ function isNotFound(error) {
4265
+ return Boolean(error && typeof error === "object" && (error.name === "ConfigNotFoundError" || error.name === "ProfileNotFoundError" || error.status === 404));
4266
+ }
4267
+ async function findConfig(store, slug) {
3871
4268
  try {
3872
- mkdirSync2(tmpDir, { recursive: true });
3873
- const proc = Bun.spawn(["tar", "xzf", absPath, "-C", tmpDir], {
3874
- stdout: "pipe",
3875
- stderr: "pipe"
4269
+ return await store.getConfig(slug);
4270
+ } catch (error) {
4271
+ if (isNotFound(error))
4272
+ return null;
4273
+ throw error;
4274
+ }
4275
+ }
4276
+ function verifyCounts(actual, expected) {
4277
+ if (canonicalDomainJson(actual) !== canonicalDomainJson(expected)) {
4278
+ throw new Error("Archive integrity failure: manifest counts do not match the domain payload");
4279
+ }
4280
+ }
4281
+ async function readV2(reader, manifest) {
4282
+ 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") {
4283
+ throw new Error("Invalid v2 archive manifest");
4284
+ }
4285
+ const payload = await reader.read("domain.json");
4286
+ if (Buffer.byteLength(payload) !== manifest.payload.size_bytes || sha2562(payload) !== manifest.payload.sha256) {
4287
+ throw new Error("Archive integrity failure: domain payload hash or size does not match the manifest");
4288
+ }
4289
+ const domain = validateInstructionsDomainArchive(JSON.parse(payload));
4290
+ const actual = computeDomainIntegrity(domain);
4291
+ verifyCounts(actual.counts, manifest.integrity.counts);
4292
+ if (canonicalDomainJson(actual) !== canonicalDomainJson(manifest.integrity)) {
4293
+ throw new Error("Archive integrity failure: logical domain hashes do not match the manifest");
4294
+ }
4295
+ return domain;
4296
+ }
4297
+ function configInput(config, content) {
4298
+ return {
4299
+ name: config.name,
4300
+ kind: config.kind,
4301
+ category: config.category,
4302
+ agent: config.agent,
4303
+ target_path: config.target_path,
4304
+ outputs: config.outputs,
4305
+ format: config.format,
4306
+ content,
4307
+ description: config.description ?? undefined,
4308
+ tags: config.tags,
4309
+ is_template: config.is_template
4310
+ };
4311
+ }
4312
+ async function restoreNewConfig(store, archived, domain) {
4313
+ 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));
4314
+ let restored = await store.createConfig(configInput(archived, archived.content));
4315
+ for (let version = 2;version <= archived.version; version++) {
4316
+ restored = await store.updateConfig(restored.id, {
4317
+ content: archived.content,
4318
+ ...version === archived.version ? {
4319
+ name: archived.name,
4320
+ kind: archived.kind,
4321
+ category: archived.category,
4322
+ agent: archived.agent,
4323
+ target_path: archived.target_path,
4324
+ outputs: archived.outputs,
4325
+ format: archived.format,
4326
+ description: archived.description,
4327
+ tags: archived.tags,
4328
+ is_template: archived.is_template,
4329
+ synced_at: archived.synced_at
4330
+ } : {}
3876
4331
  });
3877
- const exitCode = await proc.exited;
3878
- if (exitCode !== 0) {
3879
- const stderr = await new Response(proc.stderr).text();
3880
- throw new Error(`tar extraction failed: ${stderr}`);
4332
+ }
4333
+ await store.pruneSnapshots(restored.id, 0);
4334
+ for (const snapshot of snapshots) {
4335
+ await store.createSnapshot(restored.id, snapshot.content, snapshot.version);
4336
+ }
4337
+ 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));
4338
+ const expectedSnapshots = snapshots.map(({ version, content: snapshotContent }) => ({ version, content: snapshotContent })).sort((left, right) => left.version - right.version || compareText2(left.content, right.content));
4339
+ if (restored.version !== archived.version || restored.content !== archived.content || canonicalDomainJson(restoredSnapshots) !== canonicalDomainJson(expectedSnapshots)) {
4340
+ throw new Error(`restored config or snapshot history did not match archive for ${archived.slug}`);
4341
+ }
4342
+ return restored;
4343
+ }
4344
+ function rewriteAssetLocator(binding, archivedSourceId, restoredSourceId) {
4345
+ const match = /^config:\/\/([^@]+)@(\d+)$/.exec(binding.source.locator);
4346
+ if (!match)
4347
+ return binding;
4348
+ let locatorId;
4349
+ try {
4350
+ locatorId = decodeURIComponent(match[1]);
4351
+ } catch {
4352
+ return binding;
4353
+ }
4354
+ if (locatorId !== archivedSourceId)
4355
+ return binding;
4356
+ return {
4357
+ ...binding,
4358
+ source: {
4359
+ ...binding.source,
4360
+ locator: `config://${encodeURIComponent(restoredSourceId)}@${match[2]}`
3881
4361
  }
3882
- const manifestPath = join5(tmpDir, "manifest.json");
3883
- if (!existsSync5(manifestPath))
3884
- throw new Error("Invalid bundle: missing manifest.json");
3885
- const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
3886
- for (const meta of manifest.configs) {
3887
- try {
3888
- const ext = meta.format === "text" ? "txt" : meta.format;
3889
- const contentFile = join5(tmpDir, "contents", `${meta.slug}.${ext}`);
3890
- const content = existsSync5(contentFile) ? readFileSync(contentFile, "utf-8") : "";
3891
- let existing = null;
3892
- try {
3893
- existing = await store.getConfig(meta.slug);
3894
- } catch {}
3895
- if (existing) {
3896
- if (conflict === "skip") {
3897
- result.skipped++;
3898
- } else if (conflict === "overwrite" || conflict === "version") {
3899
- await store.updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs });
3900
- result.updated++;
3901
- }
3902
- } else {
3903
- await store.createConfig({
3904
- name: meta.name,
3905
- kind: meta.kind,
3906
- category: meta.category,
3907
- agent: meta.agent,
3908
- target_path: meta.target_path ?? undefined,
3909
- outputs: meta.outputs,
3910
- format: meta.format,
3911
- content,
3912
- description: meta.description ?? undefined,
3913
- tags: meta.tags,
3914
- is_template: meta.is_template
3915
- });
3916
- result.created++;
3917
- }
3918
- } catch (err) {
3919
- result.errors.push(`${meta.slug}: ${err instanceof Error ? err.message : String(err)}`);
4362
+ };
4363
+ }
4364
+ async function assertEmptyV2Destination(store) {
4365
+ const [configs, profiles, machines] = await Promise.all([
4366
+ store.listConfigs(),
4367
+ store.listProfiles(),
4368
+ store.listMachines()
4369
+ ]);
4370
+ if (configs.length > 0 || profiles.length > 0 || machines.length > 0) {
4371
+ throw new Error(`Instructions domain archive v2 recovery requires an empty destination ` + `(found configs=${configs.length}, profiles=${profiles.length}, machines=${machines.length})`);
4372
+ }
4373
+ }
4374
+ async function importV2(domain, manifest, store, conflict) {
4375
+ if (conflict !== "skip") {
4376
+ throw new Error(`Instructions domain archive v2 does not support ${conflict}; restore into an empty destination`);
4377
+ }
4378
+ await assertEmptyV2Destination(store);
4379
+ const result = emptyResult();
4380
+ result.integrity = manifest.integrity;
4381
+ const configMap = new Map;
4382
+ for (const archived of domain.configs) {
4383
+ const restored = await restoreNewConfig(store, archived, domain);
4384
+ configMap.set(archived.slug, restored);
4385
+ result.counts.configs.created++;
4386
+ result.created++;
4387
+ result.counts.config_snapshots.created += domain.config_snapshots.filter((row) => row.config_slug === archived.slug).length;
4388
+ }
4389
+ const profileMap = new Map;
4390
+ for (const archived of domain.profiles) {
4391
+ const created = await store.createProfile({
4392
+ name: archived.name,
4393
+ description: archived.description ?? undefined,
4394
+ selectors: archived.selectors,
4395
+ variables: archived.variables
4396
+ });
4397
+ profileMap.set(archived.slug, created);
4398
+ result.counts.profiles.created++;
4399
+ }
4400
+ for (const archived of domain.profiles) {
4401
+ const restoredProfile = profileMap.get(archived.slug);
4402
+ if (!restoredProfile)
4403
+ throw new Error(`post-create profile mapping missing for ${archived.slug}`);
4404
+ 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));
4405
+ 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));
4406
+ for (const row of archivedMembership) {
4407
+ const config = configMap.get(row.config_slug);
4408
+ if (!config)
4409
+ throw new Error(`missing restored config ${row.config_slug}`);
4410
+ await store.addConfigToProfile(restoredProfile.id, config.id);
4411
+ await store.setProfileConfigBinding(restoredProfile.id, config.id, row.binding);
4412
+ result.counts.profile_config_bindings.created++;
4413
+ }
4414
+ for (const row of archivedAssets) {
4415
+ const source = configMap.get(row.source_config_slug);
4416
+ const archivedSource = domain.configs.find((config) => config.slug === row.source_config_slug);
4417
+ if (!source || !archivedSource)
4418
+ throw new Error(`missing restored asset source ${row.source_config_slug}`);
4419
+ await store.addAssetToProfile(restoredProfile.id, source.id, rewriteAssetLocator(row.binding, archivedSource.id, source.id));
4420
+ result.counts.profile_asset_bindings.created++;
4421
+ }
4422
+ }
4423
+ for (const archived of domain.machines) {
4424
+ const restored = await store.registerMachine(archived.hostname, archived.os ?? undefined, archived.arch ?? undefined);
4425
+ if (archived.last_applied_at !== null)
4426
+ await store.updateMachineApplied(restored.hostname);
4427
+ result.counts.machines.created++;
4428
+ }
4429
+ const restoredDomain = await collectInstructionsDomain({ store });
4430
+ const expected = computeRestorableDomainIntegrity(domain);
4431
+ const actual = computeRestorableDomainIntegrity(restoredDomain);
4432
+ if (canonicalDomainJson(actual) !== canonicalDomainJson(expected)) {
4433
+ throw new Error(`Post-restore logical integrity verification failed: expected ${expected.domain_sha256}, got ${actual.domain_sha256}`);
4434
+ }
4435
+ return result;
4436
+ }
4437
+ async function importV1(reader, manifest, store, conflict) {
4438
+ const result = emptyResult();
4439
+ if (!Array.isArray(manifest.configs))
4440
+ throw new Error("Invalid v1 bundle: configs must be an array");
4441
+ for (const meta of manifest.configs) {
4442
+ const ext = meta.format === "text" ? "txt" : meta.format;
4443
+ const contentPath = `contents/${meta.slug}.${ext}`;
4444
+ const content = reader.members.has(contentPath) ? await reader.read(contentPath) : "";
4445
+ const existing = await findConfig(store, meta.slug);
4446
+ if (existing) {
4447
+ if (conflict === "skip") {
4448
+ result.skipped++;
4449
+ result.counts.configs.skipped++;
4450
+ } else {
4451
+ await store.updateConfig(existing.id, {
4452
+ name: meta.name,
4453
+ kind: meta.kind,
4454
+ category: meta.category,
4455
+ agent: meta.agent,
4456
+ target_path: meta.target_path,
4457
+ outputs: meta.outputs,
4458
+ format: meta.format,
4459
+ content,
4460
+ description: meta.description,
4461
+ tags: meta.tags,
4462
+ is_template: meta.is_template,
4463
+ synced_at: meta.synced_at
4464
+ });
4465
+ result.updated++;
4466
+ result.counts.configs.updated++;
3920
4467
  }
4468
+ } else {
4469
+ await store.createConfig(configInput({ ...meta, content }, content));
4470
+ result.created++;
4471
+ result.counts.configs.created++;
3921
4472
  }
3922
- return result;
3923
- } finally {
3924
- if (existsSync5(tmpDir)) {
3925
- rmSync2(tmpDir, { recursive: true, force: true });
3926
- }
3927
4473
  }
4474
+ return result;
4475
+ }
4476
+ async function importConfigs(bundlePath, opts = {}) {
4477
+ const store = opts.store ?? resolveConfigStore();
4478
+ const conflict = opts.conflict ?? "skip";
4479
+ const reader = await openArchive(resolve3(bundlePath));
4480
+ const manifestText = await reader.read("manifest.json");
4481
+ let manifest;
4482
+ try {
4483
+ manifest = JSON.parse(manifestText);
4484
+ } catch {
4485
+ throw new Error("Invalid bundle: manifest.json is not valid JSON");
4486
+ }
4487
+ if (!manifest || typeof manifest !== "object" || typeof manifest.version !== "string") {
4488
+ throw new Error("Invalid bundle: manifest version is missing");
4489
+ }
4490
+ if (manifest.version === "2.0.0") {
4491
+ const v2 = manifest;
4492
+ const domain = await readV2(reader, v2);
4493
+ return importV2(domain, v2, store, conflict);
4494
+ }
4495
+ if (manifest.version.startsWith("1.")) {
4496
+ return importV1(reader, manifest, store, conflict);
4497
+ }
4498
+ throw new Error(`Unsupported Instructions archive version: ${manifest.version}`);
3928
4499
  }
3929
4500
 
3930
4501
  // src/lib/session-apply.ts
3931
- import { createHash, randomUUID as randomUUID3 } from "crypto";
4502
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
3932
4503
  import {
3933
- existsSync as existsSync6,
4504
+ existsSync as existsSync5,
3934
4505
  lstatSync,
3935
- mkdirSync as mkdirSync3,
3936
- readFileSync as readFileSync2,
4506
+ mkdirSync as mkdirSync2,
4507
+ readFileSync,
3937
4508
  readdirSync,
3938
4509
  statSync
3939
4510
  } from "fs";
3940
- import { dirname, isAbsolute, join as join6, parse, relative, resolve as resolve4 } from "path";
4511
+ import { dirname as dirname2, isAbsolute, join as join5, parse, relative, resolve as resolve4 } from "path";
3941
4512
  class SessionApplyError extends Error {
3942
4513
  constructor(message) {
3943
4514
  super(message);
@@ -4068,8 +4639,8 @@ function assertClaudeAuthorityStillClear(plan, targetHome, ownedClaudeAuthoritie
4068
4639
  throw new SessionApplyError(`Claude authority changed after planning; refusing to apply: ${summary}`);
4069
4640
  }
4070
4641
  function ensureSessionTargetHome(targetHome) {
4071
- if (!existsSync6(targetHome))
4072
- mkdirSync3(targetHome, { recursive: true, mode: 448 });
4642
+ if (!existsSync5(targetHome))
4643
+ mkdirSync2(targetHome, { recursive: true, mode: 448 });
4073
4644
  assertSafeTargetHome(targetHome);
4074
4645
  }
4075
4646
  function checkSessionRenderDrift(targetHome, manifestPath) {
@@ -4091,7 +4662,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
4091
4662
  const drifted = [];
4092
4663
  for (const file of previousManifest.files) {
4093
4664
  const target = resolveManifestRelativePath(file.relativePath, safeTargetHome);
4094
- if (!existsSync6(target)) {
4665
+ if (!existsSync5(target)) {
4095
4666
  missing.push({
4096
4667
  path: target,
4097
4668
  relativePath: file.relativePath,
@@ -4101,7 +4672,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
4101
4672
  });
4102
4673
  continue;
4103
4674
  }
4104
- const actualSha256 = sha256(readFileSync2(target, "utf-8"));
4675
+ const actualSha256 = sha2563(readFileSync(target, "utf-8"));
4105
4676
  if (actualSha256 !== file.sha256) {
4106
4677
  drifted.push({
4107
4678
  path: target,
@@ -4248,7 +4819,7 @@ function requiredRestoreHash(file) {
4248
4819
  }
4249
4820
  function readSessionRenderSnapshot(snapshotPath) {
4250
4821
  const resolved = resolve4(snapshotPath);
4251
- if (!existsSync6(resolved))
4822
+ if (!existsSync5(resolved))
4252
4823
  throw new SessionApplyError(`Session snapshot not found: ${snapshotPath}`);
4253
4824
  const stat = lstatSync(resolved);
4254
4825
  if (stat.isSymbolicLink() || !stat.isFile()) {
@@ -4259,7 +4830,7 @@ function readSessionRenderSnapshot(snapshotPath) {
4259
4830
  }
4260
4831
  let parsed;
4261
4832
  try {
4262
- parsed = JSON.parse(readFileSync2(resolved, "utf8"));
4833
+ parsed = JSON.parse(readFileSync(resolved, "utf8"));
4263
4834
  } catch {
4264
4835
  throw new SessionApplyError(`Session snapshot is not valid JSON: ${snapshotPath}`);
4265
4836
  }
@@ -4281,7 +4852,7 @@ function readSessionRenderSnapshot(snapshotPath) {
4281
4852
  const previousManifest = snapshot.previousManifest;
4282
4853
  const previousFiles = new Map;
4283
4854
  for (const file of snapshot.files) {
4284
- if (!file || typeof file.relativePath !== "string" || typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.content !== "string" || sha256(file.content) !== file.sha256) {
4855
+ if (!file || typeof file.relativePath !== "string" || typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.content !== "string" || sha2563(file.content) !== file.sha256) {
4285
4856
  throw new SessionApplyError(`Session snapshot previous file metadata is invalid: ${snapshotPath}`);
4286
4857
  }
4287
4858
  resolveSnapshotFilePath(file.relativePath, file.path, targetHome);
@@ -4337,7 +4908,7 @@ function reconstructPreRollbackLegacyV1Snapshot(snapshot, previousFiles, previou
4337
4908
  }
4338
4909
  let parsedManifest;
4339
4910
  try {
4340
- parsedManifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
4911
+ parsedManifest = JSON.parse(readFileSync(manifestPath, "utf8"));
4341
4912
  } catch {
4342
4913
  throw new SessionApplyError(`Pre-rollback legacy v1 applied manifest is not valid JSON: ${snapshotPath}`);
4343
4914
  }
@@ -4429,15 +5000,15 @@ function assertNoNewerSessionSnapshot(snapshotPath, createdAt, targetHome) {
4429
5000
  if (!Number.isFinite(createdAtMs)) {
4430
5001
  throw new SessionApplyError(`Pre-rollback legacy v1 snapshot has an invalid creation time: ${snapshotPath}`);
4431
5002
  }
4432
- for (const entry of readdirSync(dirname(snapshotPath))) {
4433
- const candidatePath = resolve4(dirname(snapshotPath), entry);
5003
+ for (const entry of readdirSync(dirname2(snapshotPath))) {
5004
+ const candidatePath = resolve4(dirname2(snapshotPath), entry);
4434
5005
  if (candidatePath === resolve4(snapshotPath) || !entry.endsWith(".json"))
4435
5006
  continue;
4436
5007
  const candidateStat = lstatSync(candidatePath);
4437
5008
  if (candidateStat.isSymbolicLink() || !candidateStat.isFile() || candidateStat.size > 32 * 1024 * 1024)
4438
5009
  continue;
4439
5010
  try {
4440
- const candidate = JSON.parse(readFileSync2(candidatePath, "utf8"));
5011
+ const candidate = JSON.parse(readFileSync(candidatePath, "utf8"));
4441
5012
  const candidateCreatedAtMs = typeof candidate.createdAt === "string" ? Date.parse(candidate.createdAt) : Number.NaN;
4442
5013
  if ((candidate.schema === "hasna.configs.session-render-snapshot/v1" || candidate.schema === "hasna.configs.session-render-snapshot/v2") && typeof candidate.targetHome === "string" && resolve4(candidate.targetHome) === targetHome && Number.isFinite(candidateCreatedAtMs) && candidateCreatedAtMs >= createdAtMs) {
4443
5014
  throw new SessionApplyError(`Cannot restore pre-rollback legacy v1 snapshot after a newer session snapshot exists: ${candidatePath}`);
@@ -4497,7 +5068,7 @@ function inferLegacySnapshotAction(file, previousFiles, previousManifestFiles, p
4497
5068
  return "create";
4498
5069
  }
4499
5070
  if (file.role === "manifest" && previousManifest) {
4500
- const previousManifestSha256 = sha256(`${JSON.stringify(previousManifest, null, 2)}
5071
+ const previousManifestSha256 = sha2563(`${JSON.stringify(previousManifest, null, 2)}
4501
5072
  `);
4502
5073
  if (previousManifestSha256 !== file.sha256) {
4503
5074
  throw new SessionApplyError(`Cannot infer legacy v1 manifest action without a before-image: ${file.relativePath}`);
@@ -4515,8 +5086,8 @@ function resolveSnapshotFilePath(relativePath, recordedPath, targetHome) {
4515
5086
  }
4516
5087
  function planFileResult(plan, file, targetHome, previousHashes, previousManifest, options) {
4517
5088
  const target = resolvePlannedFilePath(plan, file, targetHome);
4518
- const previousContent = existsSync6(target) ? readFileSync2(target, "utf-8") : null;
4519
- const previousSha256 = previousContent === null ? null : sha256(previousContent);
5089
+ const previousContent = existsSync5(target) ? readFileSync(target, "utf-8") : null;
5090
+ const previousSha256 = previousContent === null ? null : sha2563(previousContent);
4520
5091
  const previouslyManaged = isPreviouslyManaged(file, previousSha256, previousHashes, previousManifest);
4521
5092
  const changed = previousContent !== file.content;
4522
5093
  if (previousContent !== null && !options.force && !previouslyManaged) {
@@ -4614,10 +5185,10 @@ function planStaleFileResults(plan, targetHome, previousManifest, currentRelativ
4614
5185
  }
4615
5186
  function planStaleFileResult(file, targetHome, options) {
4616
5187
  const target = resolveManifestRelativePath(file.relativePath, targetHome);
4617
- if (!existsSync6(target))
5188
+ if (!existsSync5(target))
4618
5189
  return null;
4619
- const previousContent = readFileSync2(target, "utf-8");
4620
- const previousSha256 = sha256(previousContent);
5190
+ const previousContent = readFileSync(target, "utf-8");
5191
+ const previousSha256 = sha2563(previousContent);
4621
5192
  if (!options.force && previousSha256 !== file.sha256) {
4622
5193
  return {
4623
5194
  path: target,
@@ -4682,10 +5253,10 @@ function resolveManifestRelativePath(relativePath, targetHome) {
4682
5253
  return target;
4683
5254
  }
4684
5255
  function readPreviousManifest(path) {
4685
- if (!existsSync6(path))
5256
+ if (!existsSync5(path))
4686
5257
  return null;
4687
5258
  try {
4688
- const parsed = JSON.parse(readFileSync2(path, "utf-8"));
5259
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
4689
5260
  if (parsed.schema !== SESSION_RENDER_SCHEMA)
4690
5261
  return null;
4691
5262
  if (!Array.isArray(parsed.files))
@@ -4724,13 +5295,13 @@ function assertExpectedSessionFileHash(path, targetHome, expectedHash) {
4724
5295
  }
4725
5296
  function currentSessionFileHash(path, targetHome) {
4726
5297
  assertNoSymlinkSegments(targetHome, path);
4727
- if (!existsSync6(path))
5298
+ if (!existsSync5(path))
4728
5299
  return null;
4729
5300
  const stat = lstatSync(path);
4730
5301
  if (stat.isSymbolicLink() || !stat.isFile()) {
4731
5302
  throw new SessionApplyError(`Session apply path is not a regular file: ${path}`);
4732
5303
  }
4733
- return sha256(readFileSync2(path, "utf-8"));
5304
+ return sha2563(readFileSync(path, "utf-8"));
4734
5305
  }
4735
5306
  function requiredPreviousHash(result) {
4736
5307
  if (result.previousSha256 === null) {
@@ -4739,13 +5310,13 @@ function requiredPreviousHash(result) {
4739
5310
  return result.previousSha256;
4740
5311
  }
4741
5312
  function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps) {
4742
- const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").filter((result) => existsSync6(result.path)).map((result) => {
4743
- const content = readFileSync2(result.path, "utf-8");
5313
+ const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").filter((result) => existsSync5(result.path)).map((result) => {
5314
+ const content = readFileSync(result.path, "utf-8");
4744
5315
  return {
4745
5316
  path: result.path,
4746
5317
  relativePath: result.relativePath,
4747
5318
  role: result.role,
4748
- sha256: sha256(content),
5319
+ sha256: sha2563(content),
4749
5320
  content
4750
5321
  };
4751
5322
  });
@@ -4758,7 +5329,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
4758
5329
  };
4759
5330
  }
4760
5331
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
4761
- const snapshotPath = join6(getSessionRenderSnapshotDir(targetHome), `${timestamp}-${randomUUID3()}.json`);
5332
+ const snapshotPath = join5(getSessionRenderSnapshotDir(targetHome), `${timestamp}-${randomUUID3()}.json`);
4762
5333
  const afterFiles = results.map((result) => {
4763
5334
  if (result.action === "conflict") {
4764
5335
  throw new SessionApplyError(`Cannot snapshot unresolved conflict: ${result.relativePath}`);
@@ -4811,7 +5382,7 @@ function assertSafeTargetHome(targetHome) {
4811
5382
  throw new SessionApplyError(`Session target home cannot be the filesystem root: ${targetHome}`);
4812
5383
  }
4813
5384
  assertNoSymlinkAncestors(normalized);
4814
- if (existsSync6(normalized) && lstatSync(normalized).isSymbolicLink()) {
5385
+ if (existsSync5(normalized) && lstatSync(normalized).isSymbolicLink()) {
4815
5386
  throw new SessionApplyError(`Session target home cannot be a symlink: ${normalized}`);
4816
5387
  }
4817
5388
  return normalized;
@@ -4821,8 +5392,8 @@ function assertNoSymlinkSegments(root, target) {
4821
5392
  const rel = relative(root, target);
4822
5393
  let current = root;
4823
5394
  for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
4824
- current = join6(current, segment);
4825
- if (existsSync6(current) && lstatSync(current).isSymbolicLink()) {
5395
+ current = join5(current, segment);
5396
+ if (existsSync5(current) && lstatSync(current).isSymbolicLink()) {
4826
5397
  throw new SessionApplyError(`Session apply path uses a symlink: ${current}`);
4827
5398
  }
4828
5399
  }
@@ -4833,16 +5404,16 @@ function assertNoSymlinkAncestors(path) {
4833
5404
  let current = parsed.root;
4834
5405
  const rel = relative(parsed.root, normalized);
4835
5406
  for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
4836
- current = join6(current, segment);
4837
- if (!existsSync6(current))
5407
+ current = join5(current, segment);
5408
+ if (!existsSync5(current))
4838
5409
  return;
4839
5410
  if (lstatSync(current).isSymbolicLink()) {
4840
5411
  throw new SessionApplyError(`Session apply path uses a symlink ancestor: ${current}`);
4841
5412
  }
4842
5413
  }
4843
5414
  }
4844
- function sha256(content) {
4845
- return createHash("sha256").update(content).digest("hex");
5415
+ function sha2563(content) {
5416
+ return createHash3("sha256").update(content).digest("hex");
4846
5417
  }
4847
5418
 
4848
5419
  // src/lib/global-source-coverage.ts
@@ -4878,9 +5449,9 @@ function formatGlobalSourceCoverageWarnings(result) {
4878
5449
 
4879
5450
  // src/lib/station-profile.ts
4880
5451
  import { spawnSync } from "child_process";
4881
- import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
5452
+ import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
4882
5453
  import { arch as osArch, homedir as homedir2, hostname as osHostname, platform as osPlatform, userInfo as osUserInfo } from "os";
4883
- import { dirname as dirname2, join as join7 } from "path";
5454
+ import { dirname as dirname3, join as join6 } from "path";
4884
5455
  var STATION_PROFILE_CACHE_FILENAME = "station-profile.md";
4885
5456
  var STATION_PROFILE_SOURCE_ID = "station-profile";
4886
5457
  var STATION_PROFILE_LAYER = "machine";
@@ -4894,19 +5465,19 @@ function homeDir(env = process.env) {
4894
5465
  return env["HOME"] || env["USERPROFILE"] || homedir2();
4895
5466
  }
4896
5467
  function getStationProfileCachePath(env = process.env) {
4897
- return join7(getRawStoreRoot(env), STATION_PROFILE_CACHE_FILENAME);
5468
+ return join6(getRawStoreRoot(env), STATION_PROFILE_CACHE_FILENAME);
4898
5469
  }
4899
5470
  function getMachinesManifestPath(env = process.env) {
4900
- return env[MACHINES_MANIFEST_PATH_ENV] || join7(homeDir(env), ".hasna", "machines", "machines.json");
5471
+ return env[MACHINES_MANIFEST_PATH_ENV] || join6(homeDir(env), ".hasna", "machines", "machines.json");
4901
5472
  }
4902
5473
  function getBunGlobalModulesDir(env = process.env) {
4903
- return join7(env[BUN_INSTALL_ENV] || join7(homeDir(env), ".bun"), "install", "global", "node_modules");
5474
+ return join6(env[BUN_INSTALL_ENV] || join6(homeDir(env), ".bun"), "install", "global", "node_modules");
4904
5475
  }
4905
5476
  function readMachinesManifest(path) {
4906
5477
  try {
4907
- if (!existsSync7(path))
5478
+ if (!existsSync6(path))
4908
5479
  return null;
4909
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
5480
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
4910
5481
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
4911
5482
  return null;
4912
5483
  const machines = parsed["machines"];
@@ -4962,7 +5533,7 @@ function resolveStationProfileMachine(env = process.env, options = {}) {
4962
5533
  const record = findLocalManifestMachine(readMachinesManifest(getMachinesManifestPath(env)), hostname);
4963
5534
  const home = homeDir(env);
4964
5535
  const platform = stringField(record, "platform") ?? osPlatform();
4965
- const workspacePath = stringField(record, "workspacePath") ?? join7(home, platform === "darwin" ? "Workspace" : "workspace");
5536
+ const workspacePath = stringField(record, "workspacePath") ?? join6(home, platform === "darwin" ? "Workspace" : "workspace");
4966
5537
  const machine = {
4967
5538
  id: stringField(record, "id") ?? hostname,
4968
5539
  hostname: stringField(record, "hostname") ?? hostname,
@@ -4979,9 +5550,9 @@ function resolveStationProfileMachine(env = process.env, options = {}) {
4979
5550
  return machine;
4980
5551
  }
4981
5552
  function scopedPackageNames(modulesDir, scope) {
4982
- const scopeDir = join7(modulesDir, scope);
5553
+ const scopeDir = join6(modulesDir, scope);
4983
5554
  try {
4984
- if (!existsSync7(scopeDir))
5555
+ if (!existsSync6(scopeDir))
4985
5556
  return null;
4986
5557
  return readdirNames(scopeDir).sort();
4987
5558
  } catch {
@@ -4991,7 +5562,7 @@ function scopedPackageNames(modulesDir, scope) {
4991
5562
  function readdirNames(dir) {
4992
5563
  return readdirSync2(dir).filter((name) => {
4993
5564
  try {
4994
- return lstatSync2(join7(dir, name)).isDirectory();
5565
+ return lstatSync2(join6(dir, name)).isDirectory();
4995
5566
  } catch {
4996
5567
  return false;
4997
5568
  }
@@ -5001,7 +5572,7 @@ function resolveStationProfilePackages(env = process.env) {
5001
5572
  const modulesDir = getBunGlobalModulesDir(env);
5002
5573
  let scopeDirs;
5003
5574
  try {
5004
- if (!existsSync7(modulesDir))
5575
+ if (!existsSync6(modulesDir))
5005
5576
  return null;
5006
5577
  scopeDirs = readdirNames(modulesDir).filter((name) => name.startsWith("@") && name.toLowerCase().includes("hasna"));
5007
5578
  } catch {
@@ -5076,9 +5647,9 @@ function refreshStationProfile(options = {}) {
5076
5647
  const path = getStationProfileCachePath(env);
5077
5648
  const generatedAt = new Date().toISOString();
5078
5649
  if (!options.dryRun) {
5079
- const existing = existsSync7(path) ? readFileSync3(path, "utf8") : null;
5650
+ const existing = existsSync6(path) ? readFileSync2(path, "utf8") : null;
5080
5651
  if (existing !== content) {
5081
- mkdirSync4(dirname2(path), { recursive: true });
5652
+ mkdirSync3(dirname3(path), { recursive: true });
5082
5653
  writeFileSync2(path, content, "utf8");
5083
5654
  }
5084
5655
  }
@@ -5095,9 +5666,9 @@ function refreshStationProfile(options = {}) {
5095
5666
  function readStationProfile(env = process.env) {
5096
5667
  const path = getStationProfileCachePath(env);
5097
5668
  try {
5098
- if (!existsSync7(path))
5669
+ if (!existsSync6(path))
5099
5670
  return null;
5100
- return readFileSync3(path, "utf8");
5671
+ return readFileSync2(path, "utf8");
5101
5672
  } catch {
5102
5673
  return null;
5103
5674
  }
@@ -5421,19 +5992,19 @@ async function ensureDangerousOperationGuardStandardConfig(store = resolveConfig
5421
5992
  }
5422
5993
 
5423
5994
  // src/lib/managed-skill-runtimes.ts
5424
- import { createHash as createHash2 } from "crypto";
5995
+ import { createHash as createHash4 } from "crypto";
5425
5996
  import { spawnSync as spawnSync2 } from "child_process";
5426
5997
  import {
5427
- existsSync as existsSync8,
5998
+ existsSync as existsSync7,
5428
5999
  lstatSync as lstatSync3,
5429
- mkdirSync as mkdirSync5,
5430
- readFileSync as readFileSync4,
6000
+ mkdirSync as mkdirSync4,
6001
+ readFileSync as readFileSync3,
5431
6002
  renameSync,
5432
- rmSync as rmSync3,
6003
+ rmSync as rmSync2,
5433
6004
  writeFileSync as writeFileSync3
5434
6005
  } from "fs";
5435
6006
  import { homedir as homedir3 } from "os";
5436
- import { dirname as dirname3, join as join8, parse as parse2, relative as relative2, resolve as resolve5 } from "path";
6007
+ import { dirname as dirname4, join as join7, parse as parse2, relative as relative2, resolve as resolve5 } from "path";
5437
6008
  var INBOX_CONVERSATIONS_MINIMUM_VERSION = "0.5.28";
5438
6009
  var INBOX_SKILL_MARKERS = [
5439
6010
  [".claude", "skills", "inbox", "SKILL.md"],
@@ -5443,8 +6014,8 @@ var INBOX_SKILL_MARKERS = [
5443
6014
  [".cursor", "skills", "inbox", "SKILL.md"]
5444
6015
  ];
5445
6016
  var REQUIRED_WATCH_FLAGS = ["--from <agent>", "--all", "--full-content"];
5446
- function sha2562(content) {
5447
- return createHash2("sha256").update(content).digest("hex");
6017
+ function sha2564(content) {
6018
+ return createHash4("sha256").update(content).digest("hex");
5448
6019
  }
5449
6020
  function lstatOrNull(path) {
5450
6021
  try {
@@ -5459,8 +6030,8 @@ function findSymlinkedAncestor(path) {
5459
6030
  let current = parsed.root;
5460
6031
  const rel = relative2(parsed.root, normalized);
5461
6032
  for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
5462
- current = join8(current, segment);
5463
- if (!existsSync8(current))
6033
+ current = join7(current, segment);
6034
+ if (!existsSync7(current))
5464
6035
  return null;
5465
6036
  if (lstatSync3(current).isSymbolicLink())
5466
6037
  return current;
@@ -5476,16 +6047,7 @@ function assertNoSymlinkAncestors2(path) {
5476
6047
  function packagedInboxSkillPath(explicitPath) {
5477
6048
  if (explicitPath)
5478
6049
  return explicitPath;
5479
- const candidates = [
5480
- join8(import.meta.dir, "..", "..", "assets", "skills", "inbox", "SKILL.md"),
5481
- join8(import.meta.dir, "..", "assets", "skills", "inbox", "SKILL.md"),
5482
- join8(process.cwd(), "assets", "skills", "inbox", "SKILL.md")
5483
- ];
5484
- const found = candidates.find((candidate) => existsSync8(candidate));
5485
- if (!found) {
5486
- throw new Error(`packaged inbox skill contract is missing (checked ${candidates.length} package-relative locations)`);
5487
- }
5488
- return found;
6050
+ throw new Error("Bundled skill contracts are retired; use the Skills CLI to manage private skills");
5489
6051
  }
5490
6052
  function readCanonicalSkill(explicitPath) {
5491
6053
  const assetPath = packagedInboxSkillPath(explicitPath);
@@ -5493,14 +6055,14 @@ function readCanonicalSkill(explicitPath) {
5493
6055
  if (!stat?.isFile()) {
5494
6056
  throw new Error("packaged inbox skill contract is not a regular file");
5495
6057
  }
5496
- const content = readFileSync4(assetPath, "utf8");
6058
+ const content = readFileSync3(assetPath, "utf8");
5497
6059
  if (!content.includes("conversations watch --from <agent> --all")) {
5498
6060
  throw new Error("packaged inbox skill contract does not declare the canonical conversations watcher");
5499
6061
  }
5500
6062
  if (!content.includes("There is no separate")) {
5501
6063
  throw new Error("packaged inbox skill contract does not retire the legacy executable");
5502
6064
  }
5503
- return { content, sha256: sha2562(content) };
6065
+ return { content, sha256: sha2564(content) };
5504
6066
  }
5505
6067
  function runProbe(command, args) {
5506
6068
  const result = spawnSync2(command, args, {
@@ -5531,7 +6093,7 @@ function compareVersions(left, right) {
5531
6093
  return 0;
5532
6094
  }
5533
6095
  function inspectSkillMarkers(homeDir2) {
5534
- return INBOX_SKILL_MARKERS.map((parts) => join8(homeDir2, ...parts)).map((path) => {
6096
+ return INBOX_SKILL_MARKERS.map((parts) => join7(homeDir2, ...parts)).map((path) => {
5535
6097
  const stat = lstatOrNull(path);
5536
6098
  if (!stat)
5537
6099
  return null;
@@ -5540,7 +6102,7 @@ function inspectSkillMarkers(homeDir2) {
5540
6102
  }
5541
6103
  return {
5542
6104
  path,
5543
- content: readFileSync4(path, "utf8"),
6105
+ content: readFileSync3(path, "utf8"),
5544
6106
  mode: stat.mode & 511,
5545
6107
  regular: true
5546
6108
  };
@@ -5575,7 +6137,7 @@ function inspectInbox(options) {
5575
6137
  let reason = "skill not installed";
5576
6138
  if (skillPresent) {
5577
6139
  const nonRegular = snapshots.some((snapshot) => !snapshot.regular);
5578
- const symlinkAncestor = snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname3(path))).find((found) => found !== null);
6140
+ const symlinkAncestor = snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname4(path))).find((found) => found !== null);
5579
6141
  if (nonRegular)
5580
6142
  reason = "managed skill target is not a regular file";
5581
6143
  else if (symlinkAncestor)
@@ -5659,13 +6221,13 @@ function projectUpdatedStatus(status, contractCount) {
5659
6221
  };
5660
6222
  }
5661
6223
  function cleanup(path) {
5662
- rmSync3(path, { force: true });
6224
+ rmSync2(path, { force: true });
5663
6225
  }
5664
6226
  function writeAtomic(path, content, mode) {
5665
- assertNoSymlinkAncestors2(dirname3(path));
6227
+ assertNoSymlinkAncestors2(dirname4(path));
5666
6228
  const tempPath = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
5667
6229
  try {
5668
- mkdirSync5(dirname3(path), { recursive: true, mode: 493 });
6230
+ mkdirSync4(dirname4(path), { recursive: true, mode: 493 });
5669
6231
  writeFileSync3(tempPath, content, { mode, flag: "wx" });
5670
6232
  renameSync(tempPath, path);
5671
6233
  } finally {
@@ -5674,7 +6236,7 @@ function writeAtomic(path, content, mode) {
5674
6236
  }
5675
6237
  var DEFAULT_SKILL_WRITE_FILE_OPERATIONS = {
5676
6238
  lstat: lstatOrNull,
5677
- read: (path) => readFileSync4(path, "utf8"),
6239
+ read: (path) => readFileSync3(path, "utf8"),
5678
6240
  write: writeAtomic
5679
6241
  };
5680
6242
  function writeSkillContractsTransactional(snapshots, canonicalContent, fileOperations = DEFAULT_SKILL_WRITE_FILE_OPERATIONS) {
@@ -5741,7 +6303,7 @@ async function reconcileManagedSkillRuntimes(options = {}) {
5741
6303
  dry_run: dryRun
5742
6304
  };
5743
6305
  }
5744
- const symlinkedAncestor = before.snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname3(path))).find((found) => found !== null);
6306
+ const symlinkedAncestor = before.snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname4(path))).find((found) => found !== null);
5745
6307
  if (symlinkedAncestor) {
5746
6308
  return {
5747
6309
  runtimes: [{ ...status, action: "failed", dry_run: dryRun, skill_contracts_changed: 0 }],
@@ -5818,28 +6380,28 @@ async function reconcileManagedSkillRuntimes(options = {}) {
5818
6380
  }
5819
6381
 
5820
6382
  // src/status.ts
5821
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
6383
+ import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
5822
6384
 
5823
6385
  // src/lib/package-version.ts
5824
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
5825
- import { dirname as dirname4, join as join9 } from "path";
6386
+ import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
6387
+ import { dirname as dirname5, join as join8 } from "path";
5826
6388
  import { fileURLToPath } from "url";
5827
6389
  var cached = null;
5828
6390
  function getPackageVersion() {
5829
6391
  if (cached)
5830
6392
  return cached;
5831
6393
  try {
5832
- let dir = dirname4(fileURLToPath(import.meta.url));
6394
+ let dir = dirname5(fileURLToPath(import.meta.url));
5833
6395
  for (let i = 0;i < 8; i++) {
5834
- const pkgPath = join9(dir, "package.json");
5835
- if (existsSync9(pkgPath)) {
5836
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
6396
+ const pkgPath = join8(dir, "package.json");
6397
+ if (existsSync8(pkgPath)) {
6398
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5837
6399
  if (pkg.name === "@hasna/instructions" && pkg.version) {
5838
6400
  cached = pkg.version;
5839
6401
  return cached;
5840
6402
  }
5841
6403
  }
5842
- const parent = dirname4(dir);
6404
+ const parent = dirname5(dir);
5843
6405
  if (parent === dir)
5844
6406
  break;
5845
6407
  dir = parent;
@@ -5908,11 +6470,11 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
5908
6470
  continue;
5909
6471
  knownTargets += 1;
5910
6472
  const targetPath = expandPath(config.target_path);
5911
- if (!existsSync10(targetPath)) {
6473
+ if (!existsSync9(targetPath)) {
5912
6474
  missingTargets += 1;
5913
6475
  continue;
5914
6476
  }
5915
- const disk = readFileSync6(targetPath, "utf-8");
6477
+ const disk = readFileSync5(targetPath, "utf-8");
5916
6478
  const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(config.target_path, config.format));
5917
6479
  if (redactedDisk !== config.content) {
5918
6480
  driftedTargets += 1;
@@ -6004,9 +6566,9 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
6004
6566
  }
6005
6567
 
6006
6568
  // src/lib/provider-context.ts
6007
- import { createHash as createHash3 } from "crypto";
6008
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
6009
- import { join as join10 } from "path";
6569
+ import { createHash as createHash5 } from "crypto";
6570
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
6571
+ import { join as join9 } from "path";
6010
6572
  var PROVIDER_CONTEXT_DIR = ".hasna/provider-context";
6011
6573
  var PROVIDER_CONTEXT_MANIFEST = "manifest.json";
6012
6574
  var PROVIDER_CONTEXT_SCHEMA = "hasna.instructions.provider-context/v1";
@@ -6141,8 +6703,8 @@ function renderPerEndpointFragment(entry) {
6141
6703
  function renderProviderFragment(entry) {
6142
6704
  return entry ? renderPerEndpointFragment(entry) : INVARIANT_FRAGMENT;
6143
6705
  }
6144
- function sha2563(content) {
6145
- return createHash3("sha256").update(content).digest("hex");
6706
+ function sha2565(content) {
6707
+ return createHash5("sha256").update(content).digest("hex");
6146
6708
  }
6147
6709
  function resolveAndRenderProviderContext(opts) {
6148
6710
  const entry = matchProviderEndpoint(opts.origin);
@@ -6151,18 +6713,18 @@ function resolveAndRenderProviderContext(opts) {
6151
6713
  const recordedEndpoint = originAccepted ? `${opts.origin.host}${opts.origin.pathPrefix || ""}` : null;
6152
6714
  const reason = entry === null && opts.rawEndpoint ? originAccepted ? `endpoint "${recordedEndpoint}" is not in the provider-context registry; using the invariant fragment` : "endpoint rejected (embedded credentials or unparseable); using the invariant fragment" : null;
6153
6715
  const content = renderProviderFragment(entry);
6154
- const dir = join10(opts.homeDir, PROVIDER_CONTEXT_DIR);
6155
- if (!existsSync11(dir))
6156
- mkdirSync6(dir, { recursive: true });
6716
+ const dir = join9(opts.homeDir, PROVIDER_CONTEXT_DIR);
6717
+ if (!existsSync10(dir))
6718
+ mkdirSync5(dir, { recursive: true });
6157
6719
  const filename = `${entry ? entry.key : "invariant"}.md`;
6158
- const fragmentPath = join10(dir, filename);
6159
- const fragmentSha256 = sha2563(content);
6720
+ const fragmentPath = join9(dir, filename);
6721
+ const fragmentSha256 = sha2565(content);
6160
6722
  writeFileSync4(fragmentPath, content, "utf8");
6161
- const manifestPath = join10(dir, PROVIDER_CONTEXT_MANIFEST);
6723
+ const manifestPath = join9(dir, PROVIDER_CONTEXT_MANIFEST);
6162
6724
  let manifest = { schema: PROVIDER_CONTEXT_SCHEMA, fragments: {} };
6163
6725
  try {
6164
- if (existsSync11(manifestPath)) {
6165
- const parsed = JSON.parse(readFileSync7(manifestPath, "utf8"));
6726
+ if (existsSync10(manifestPath)) {
6727
+ const parsed = JSON.parse(readFileSync6(manifestPath, "utf8"));
6166
6728
  if (parsed && typeof parsed === "object")
6167
6729
  manifest = parsed;
6168
6730
  }
@@ -6228,6 +6790,9 @@ function printLine(text = "") {
6228
6790
  function printJson(value) {
6229
6791
  printLine(JSON.stringify(value, null, 2));
6230
6792
  }
6793
+ function printMachineJson(value, pretty = false) {
6794
+ printLine(JSON.stringify(value, null, pretty ? 2 : undefined));
6795
+ }
6231
6796
  function printManagedSkillRuntimeReport(report) {
6232
6797
  for (const runtime of report.runtimes) {
6233
6798
  if (!runtime.skill_present)
@@ -6249,7 +6814,7 @@ function fmtConfig(c, format) {
6249
6814
  ` ${chalk.cyan("category:")} ${c.category} ${chalk.cyan("agent:")} ${c.agent} ${chalk.cyan("kind:")} ${c.kind}`,
6250
6815
  ` ${chalk.cyan("format:")} ${c.format} ${chalk.cyan("version:")} ${c.version}${c.target_path ? ` ${chalk.cyan("path:")} ${c.target_path}` : ""}`,
6251
6816
  c.description ? ` ${chalk.dim(c.description)}` : "",
6252
- c.tags.length > 0 ? ` ${chalk.dim("tags: " + c.tags.join(", "))}` : ""
6817
+ c.tags?.length ? ` ${chalk.dim("tags: " + c.tags.join(", "))}` : ""
6253
6818
  ].filter(Boolean).join(`
6254
6819
  `);
6255
6820
  }
@@ -6267,7 +6832,7 @@ function printConfigRows(configs) {
6267
6832
  for (const c of configs) {
6268
6833
  const type = `${c.category}/${c.agent}`;
6269
6834
  const path = c.kind === "reference" ? "(ref)" : c.target_path ?? "(no path)";
6270
- console.log(`${pad(c.slug, 32)} ${pad(type, 15)} ${pad(c.format, 8)} ${pad(truncateMiddle(path, 44), 44)} ${String(c.outputs.length).padStart(3)} ${c.version}`);
6835
+ console.log(`${pad(c.slug, 32)} ${pad(type, 15)} ${pad(c.format, 8)} ${pad(truncateMiddle(path, 44), 44)} ${String(c.output_count).padStart(3)} ${c.version}`);
6271
6836
  }
6272
6837
  }
6273
6838
  function splitCsv(value) {
@@ -6305,7 +6870,7 @@ function parseSessionSource(value, order) {
6305
6870
  if (!path)
6306
6871
  throw new Error(`Invalid --source "${value}" (expected path or id=path)`);
6307
6872
  const absPath = resolveSessionPath(path);
6308
- if (!existsSync12(absPath))
6873
+ if (!existsSync11(absPath))
6309
6874
  throw new Error(`Instruction source file not found: ${absPath}`);
6310
6875
  const content = readSessionInstructionSourceFile(absPath);
6311
6876
  const source = sourceFromFilePath(absPath, content, order);
@@ -6359,7 +6924,7 @@ function readSessionInstructionSourceFile(path) {
6359
6924
  if (stat.size > SESSION_MANAGED_INPUT_MAX_BYTES) {
6360
6925
  throw new Error(`SESSION_SOURCE_INPUT_TOO_LARGE: instruction source file exceeds ${SESSION_MANAGED_INPUT_MAX_BYTES} bytes`);
6361
6926
  }
6362
- return readFileSync8(path, "utf-8");
6927
+ return readFileSync7(path, "utf-8");
6363
6928
  }
6364
6929
  function parseLayeredReference(value) {
6365
6930
  const trimmed = value.trim();
@@ -6386,9 +6951,9 @@ async function collectSessionSources(opts, tool, store) {
6386
6951
  }
6387
6952
  for (const value of opts.identityExport ?? []) {
6388
6953
  const path = resolveSessionPath(value);
6389
- if (!existsSync12(path))
6954
+ if (!existsSync11(path))
6390
6955
  throw new Error(`Identity instruction export not found: ${path}`);
6391
- const parsed = JSON.parse(readFileSync8(path, "utf-8"));
6956
+ const parsed = JSON.parse(readFileSync7(path, "utf-8"));
6392
6957
  sources.push(...sourcesFromIdentityExport(parsed, { path, tool, orderOffset: sources.length }));
6393
6958
  }
6394
6959
  return sources.map((source) => {
@@ -6520,7 +7085,7 @@ function readProjectContextBundleOption(value, allowMissing = false) {
6520
7085
  if (value === "-")
6521
7086
  return { json: readBoundedProjectContextStdin() };
6522
7087
  const path = resolveSessionPath(value);
6523
- if (!existsSync12(path)) {
7088
+ if (!existsSync11(path)) {
6524
7089
  if (allowMissing)
6525
7090
  return {};
6526
7091
  throw new ProjectContextError("PROJECT_CONTEXT_INPUT_MISSING", `bundle file not found: ${path}`);
@@ -6532,7 +7097,7 @@ function readProjectContextBundleOption(value, allowMissing = false) {
6532
7097
  if (stat.size > PROJECT_CONTEXT_MAX_INPUT_BYTES) {
6533
7098
  throw new ProjectContextError("PROJECT_CONTEXT_INPUT_TOO_LARGE", `bundle exceeds ${PROJECT_CONTEXT_MAX_INPUT_BYTES} bytes`);
6534
7099
  }
6535
- return { json: readFileSync8(path, "utf8"), sourcePath: path };
7100
+ return { json: readFileSync7(path, "utf8"), sourcePath: path };
6536
7101
  }
6537
7102
  function readBoundedProjectContextStdin() {
6538
7103
  const chunks = [];
@@ -6619,24 +7184,245 @@ async function getMachineProfileContext(opts, store, readOptions = {}) {
6619
7184
  const profile = resolution.profile;
6620
7185
  return { machine, profile, resolution, vars: resolveProfileVariables(profile, machine) };
6621
7186
  }
6622
- program.command("list").alias("ls").description("List stored configs").option("-c, --category <cat>", "filter by category").option("-a, --agent <agent>", "filter by agent").option("-k, --kind <kind>", "filter by kind (file|reference)").option("-t, --tag <tag>", "filter by tag").option("-s, --search <query>", "search name/description/content").option("-f, --format <fmt>", "output format: compact|table|json", "compact").option("--brief", "shorthand for --format compact").option("--verbose", "show expanded metadata for each listed config").option("--json", "output full matching records as JSON").option("--limit <n>", `max rows for human output (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based pagination cursor for human output").action(async (opts) => {
6623
- const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
6624
- const configs = await resolveConfigStore().listConfigs({
7187
+ var COMPACT_CONFIG_FIELDS = [
7188
+ "id",
7189
+ "slug",
7190
+ "name",
7191
+ "kind",
7192
+ "category",
7193
+ "agent",
7194
+ "format",
7195
+ "is_template",
7196
+ "version",
7197
+ "updated_at"
7198
+ ];
7199
+ var OPTIONAL_CONFIG_IDENTITY_FIELDS = ["created_at", "synced_at"];
7200
+ var FULL_CONFIG_FIELDS = [
7201
+ "id",
7202
+ "name",
7203
+ "slug",
7204
+ "kind",
7205
+ "category",
7206
+ "agent",
7207
+ "target_path",
7208
+ "outputs",
7209
+ "format",
7210
+ "content",
7211
+ "description",
7212
+ "tags",
7213
+ "is_template",
7214
+ "version",
7215
+ "created_at",
7216
+ "updated_at",
7217
+ "synced_at"
7218
+ ];
7219
+ function parseConfigListDetail(value) {
7220
+ if (value === undefined)
7221
+ return;
7222
+ if (value === "compact" || value === "full")
7223
+ return value;
7224
+ throw new Error('--detail must be "compact" or "full"');
7225
+ }
7226
+ function parseConfigListFields(value, detail) {
7227
+ if (value === undefined)
7228
+ return;
7229
+ const raw = String(value);
7230
+ if (raw.length > 1024)
7231
+ throw new Error("--fields exceeds 1024 characters");
7232
+ const fields = raw.split(",").map((field) => field.trim());
7233
+ if (fields.length === 0 || fields.some((field) => field.length === 0)) {
7234
+ throw new Error("--fields must contain non-empty comma-separated field names");
7235
+ }
7236
+ if (fields.length > 32)
7237
+ throw new Error("--fields accepts at most 32 fields");
7238
+ if (new Set(fields).size !== fields.length)
7239
+ throw new Error("--fields must not contain duplicate names");
7240
+ const allowed = new Set(detail === "full" ? FULL_CONFIG_FIELDS : [...COMPACT_CONFIG_FIELDS, ...OPTIONAL_CONFIG_IDENTITY_FIELDS]);
7241
+ const unknown = fields.filter((field) => !allowed.has(field));
7242
+ if (unknown.length > 0)
7243
+ throw new Error(`Unknown ${detail} config field(s): ${unknown.join(", ")}`);
7244
+ if (!fields.includes("id"))
7245
+ throw new Error("--fields must include the immutable id field");
7246
+ if (detail !== "full" && fields.includes("content")) {
7247
+ throw new Error("Config content requires --detail full");
7248
+ }
7249
+ return fields;
7250
+ }
7251
+ function projectConfigFields(record, fields) {
7252
+ return Object.fromEntries(fields.map((field) => [field, record[field]]));
7253
+ }
7254
+ async function readAllConfigIdentityPages(store, filter) {
7255
+ const items = [];
7256
+ const seen = new Set;
7257
+ let cursor = 0;
7258
+ let expectedTotal = null;
7259
+ let sourceBounded = true;
7260
+ let previousId = null;
7261
+ while (true) {
7262
+ const page = await store.listConfigIdentitiesPage(filter, { limit: 100, cursor });
7263
+ sourceBounded = sourceBounded && page.source_bounded;
7264
+ if (expectedTotal === null)
7265
+ expectedTotal = page.total;
7266
+ else if (page.total !== expectedTotal)
7267
+ throw new Error("Config collection changed while paging");
7268
+ for (const item of page.items) {
7269
+ if (seen.has(item.id))
7270
+ throw new Error(`Config collection repeated ${item.id} while paging`);
7271
+ if (page.source_bounded && previousId !== null && item.id <= previousId)
7272
+ throw new Error("Config collection order changed while paging");
7273
+ seen.add(item.id);
7274
+ previousId = page.source_bounded ? item.id : null;
7275
+ items.push(item);
7276
+ }
7277
+ if (page.next_cursor === null)
7278
+ break;
7279
+ if (page.next_cursor <= cursor)
7280
+ throw new Error("Config identity pagination did not advance");
7281
+ cursor = page.next_cursor;
7282
+ }
7283
+ if (expectedTotal !== null && items.length !== expectedTotal) {
7284
+ throw new Error(`Config collection returned ${items.length} of ${expectedTotal} rows`);
7285
+ }
7286
+ return { items, source_bounded: sourceBounded };
7287
+ }
7288
+ async function readAllConfigPages(store, filter) {
7289
+ const items = [];
7290
+ const seen = new Set;
7291
+ let cursor = 0;
7292
+ let expectedTotal = null;
7293
+ let sourceBounded = true;
7294
+ let previousId = null;
7295
+ while (true) {
7296
+ const page = await store.listConfigsPage(filter, { limit: 100, cursor });
7297
+ sourceBounded = sourceBounded && page.source_bounded;
7298
+ if (expectedTotal === null)
7299
+ expectedTotal = page.total;
7300
+ else if (page.total !== expectedTotal)
7301
+ throw new Error("Config collection changed while paging");
7302
+ for (const item of page.items) {
7303
+ if (seen.has(item.id))
7304
+ throw new Error(`Config collection repeated ${item.id} while paging`);
7305
+ if (page.source_bounded && previousId !== null && item.id <= previousId)
7306
+ throw new Error("Config collection order changed while paging");
7307
+ seen.add(item.id);
7308
+ previousId = page.source_bounded ? item.id : null;
7309
+ items.push(item);
7310
+ }
7311
+ if (page.next_cursor === null)
7312
+ break;
7313
+ if (page.next_cursor <= cursor)
7314
+ throw new Error("Config pagination did not advance");
7315
+ cursor = page.next_cursor;
7316
+ }
7317
+ if (expectedTotal !== null && items.length !== expectedTotal) {
7318
+ throw new Error(`Config collection returned ${items.length} of ${expectedTotal} rows`);
7319
+ }
7320
+ return { items, source_bounded: sourceBounded };
7321
+ }
7322
+ function configListEnvelope(records, page, detail, fields) {
7323
+ const complete = page.cursor === 0 && !page.has_more;
7324
+ const truncated = !complete;
7325
+ const truncationReason = page.cursor > 0 ? page.has_more ? "cursor_and_limit" : "cursor" : page.has_more ? "limit" : null;
7326
+ return {
7327
+ configs: records,
7328
+ _meta: {
7329
+ schema_version: 1,
7330
+ count: records.length,
7331
+ total: page.total,
7332
+ limit: page.limit,
7333
+ cursor: page.cursor,
7334
+ next_cursor: page.next_cursor,
7335
+ has_more: page.has_more,
7336
+ complete,
7337
+ truncated,
7338
+ truncation_reason: truncationReason,
7339
+ detail,
7340
+ fields: [...fields],
7341
+ source_bounded: page.source_bounded
7342
+ }
7343
+ };
7344
+ }
7345
+ program.command("list").alias("ls").description("List stored configs").option("-c, --category <cat>", "filter by category").option("-a, --agent <agent>", "filter by agent").option("-k, --kind <kind>", "filter by kind (file|reference)").option("-t, --tag <tag>", "filter by tag").option("-s, --search <query>", "search name/description/content").option("-f, --format <fmt>", "output format: compact|table|json", "compact").option("--brief", "shorthand for --format compact").option("--verbose", "show expanded metadata for each listed config").option("--json", "output machine-readable records (legacy default: complete full array)").option("--detail <level>", "JSON detail: compact|full; enables a bounded metadata envelope").option("--fields <fields>", "comma-separated JSON fields; implies --detail compact").option("--all", "explicitly read every matching row").option("--pretty", "pretty-print modern JSON output (modern JSON is compact by default)").option("--limit <n>", `max rows (default ${DEFAULT_LIST_LIMIT} for bounded output)`).option("--cursor <n>", "zero-based pagination cursor").action(async (opts) => {
7346
+ const requestedDetail = parseConfigListDetail(opts.detail);
7347
+ const modernJson = requestedDetail !== undefined || opts.fields !== undefined;
7348
+ const jsonRequested = Boolean(opts.json || opts.format === "json");
7349
+ if (modernJson && !jsonRequested)
7350
+ throw new Error("--detail and --fields require JSON output");
7351
+ if (opts.all && !modernJson)
7352
+ throw new Error("--all requires JSON output with --detail or --fields");
7353
+ if (opts.pretty && !jsonRequested)
7354
+ throw new Error("--pretty requires JSON output");
7355
+ const detail = requestedDetail ?? "compact";
7356
+ const fields = parseConfigListFields(opts.fields, detail) ?? (detail === "full" ? [...FULL_CONFIG_FIELDS] : [...COMPACT_CONFIG_FIELDS]);
7357
+ const filter = {
6625
7358
  category: opts.category,
6626
7359
  agent: opts.agent,
6627
7360
  kind: opts.kind,
6628
7361
  tags: opts.tag ? [opts.tag] : undefined,
6629
7362
  search: opts.search
6630
- });
7363
+ };
7364
+ const store = resolveConfigStore();
7365
+ if (modernJson) {
7366
+ if (opts.all && (opts.cursor !== undefined || opts.limit !== undefined)) {
7367
+ throw new Error("--all cannot be combined with --limit or --cursor");
7368
+ }
7369
+ let page2;
7370
+ if (opts.all) {
7371
+ if (detail === "full") {
7372
+ const completeRead = await readAllConfigPages(store, filter);
7373
+ page2 = {
7374
+ items: completeRead.items,
7375
+ total: completeRead.items.length,
7376
+ limit: Math.max(completeRead.items.length, 1),
7377
+ cursor: 0,
7378
+ next_cursor: null,
7379
+ has_more: false,
7380
+ complete: true,
7381
+ truncated: false,
7382
+ source_bounded: completeRead.source_bounded
7383
+ };
7384
+ } else {
7385
+ const completeRead = await readAllConfigIdentityPages(store, filter);
7386
+ page2 = {
7387
+ items: completeRead.items,
7388
+ total: completeRead.items.length,
7389
+ limit: Math.max(completeRead.items.length, 1),
7390
+ cursor: 0,
7391
+ next_cursor: null,
7392
+ has_more: false,
7393
+ complete: true,
7394
+ truncated: false,
7395
+ source_bounded: completeRead.source_bounded
7396
+ };
7397
+ }
7398
+ } else {
7399
+ page2 = detail === "full" ? await store.listConfigsPage(filter, { limit: opts.limit, cursor: opts.cursor }) : await store.listConfigIdentitiesPage(filter, { limit: opts.limit, cursor: opts.cursor });
7400
+ }
7401
+ const records = page2.items.map((item) => projectConfigFields(item, fields));
7402
+ printMachineJson(configListEnvelope(records, {
7403
+ total: page2.total,
7404
+ limit: opts.all ? null : page2.limit,
7405
+ cursor: page2.cursor,
7406
+ next_cursor: page2.next_cursor,
7407
+ has_more: page2.has_more,
7408
+ complete: page2.complete,
7409
+ source_bounded: page2.source_bounded
7410
+ }, detail, fields), Boolean(opts.pretty));
7411
+ return;
7412
+ }
7413
+ const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
6631
7414
  if (fmt === "json") {
7415
+ const configs = opts.limit !== undefined || opts.cursor !== undefined ? (await store.listConfigsPage(filter, { limit: opts.limit, cursor: opts.cursor })).items : await store.listConfigs(filter);
6632
7416
  printJson(configs);
6633
7417
  return;
6634
7418
  }
6635
- if (configs.length === 0) {
7419
+ const page = opts.all ? (() => {
7420
+ throw new Error("--all is only available with --json and --detail");
7421
+ })() : await store.listConfigSummariesPage(filter, { limit: opts.limit, cursor: opts.cursor });
7422
+ if (page.total === 0) {
6636
7423
  console.log(chalk.dim("No configs found."));
6637
7424
  return;
6638
7425
  }
6639
- const page = paginate(configs, { limit: opts.limit, cursor: opts.cursor });
6640
7426
  if (fmt === "compact") {
6641
7427
  printConfigRows(page.items);
6642
7428
  } else {
@@ -6645,7 +7431,7 @@ program.command("list").alias("ls").description("List stored configs").option("-
6645
7431
  console.log();
6646
7432
  }
6647
7433
  }
6648
- pageFooter("configs list", page, "Use --verbose for expanded rows, --json for full records, or `configs show <slug>` for content.");
7434
+ pageFooter("configs list", page, "Use --verbose for expanded rows, --json --detail compact for metadata, or `configs show <slug>` for content.");
6649
7435
  });
6650
7436
  program.command("show <id>").alias("inspect").description("Show a config's content and metadata").option("-f, --format <fmt>", "output format: table|json|content", "table").action(async (id, opts) => {
6651
7437
  try {
@@ -6692,15 +7478,15 @@ program.command("tag <id>").description("Add or remove tags on a stored config (
6692
7478
  });
6693
7479
  program.command("add <path>").description("Ingest a file into the config DB").option("-n, --name <name>", "config name (defaults to filename)").option("-c, --category <cat>", "category override").option("-a, --agent <agent>", "agent override").option("-k, --kind <kind>", "kind: file|reference", "file").option("--template", "mark as template (has {{VAR}} placeholders)").option("--update", "if a config already owns this path, update that row in place instead of refusing").action(async (filePath, opts) => {
6694
7480
  const abs = resolve6(filePath);
6695
- if (!existsSync12(abs)) {
7481
+ if (!existsSync11(abs)) {
6696
7482
  console.error(chalk.red(`File not found: ${abs}`));
6697
7483
  process.exit(1);
6698
7484
  }
6699
- const rawContent = readFileSync8(abs, "utf-8");
7485
+ const rawContent = readFileSync7(abs, "utf-8");
6700
7486
  const storedFmt = detectFormat(abs);
6701
7487
  const fmt = redactFormatForTarget(abs, storedFmt);
6702
7488
  const { content, redacted, isTemplate } = redactContent(rawContent, fmt);
6703
- const targetPath = abs.startsWith(homedir4()) ? abs.replace(homedir4(), "~") : abs;
7489
+ const targetPath = compactPathForConfigHome(abs);
6704
7490
  const name = opts.name || filePath.split("/").pop();
6705
7491
  const store = resolveConfigStore();
6706
7492
  const allConfigs = await store.listConfigs();
@@ -6875,7 +7661,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
6875
7661
  for (const entry of entries) {
6876
7662
  if (!entry.isDirectory())
6877
7663
  continue;
6878
- const projDir = join11(absDir, entry.name);
7664
+ const projDir = join10(absDir, entry.name);
6879
7665
  const hasAgentConfig = [
6880
7666
  "CLAUDE.md",
6881
7667
  ".mcp.json",
@@ -6888,7 +7674,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
6888
7674
  ".aicopilot",
6889
7675
  ".cursor",
6890
7676
  ".agents"
6891
- ].some((marker) => existsSync12(join11(projDir, marker)));
7677
+ ].some((marker) => existsSync11(join10(projDir, marker)));
6892
7678
  if (!hasAgentConfig)
6893
7679
  continue;
6894
7680
  const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun, store });
@@ -6918,28 +7704,25 @@ program.command("sync").description("Sync known AI coding configs from disk into
6918
7704
  }
6919
7705
  }
6920
7706
  });
6921
- program.command("export").description("Export configs as a tar.gz bundle").option("-o, --output <path>", "output file", "./configs-export.tar.gz").option("-c, --category <cat>", "filter by category").action(async (opts) => {
7707
+ program.command("export").description("Export the complete Instructions configuration domain as a tar.gz bundle").option("-o, --output <path>", "output file", "./instructions-domain-export.tar.gz").option("-c, --category <cat>", "filter by category").action(async (opts) => {
6922
7708
  const result = await exportConfigs(opts.output, {
6923
7709
  filter: opts.category ? { category: opts.category } : undefined,
6924
7710
  store: resolveConfigStore()
6925
7711
  });
6926
- console.log(chalk.green("\u2713") + ` Exported ${result.count} configs to ${result.path}`);
7712
+ console.log(chalk.green("\u2713") + ` Exported domain to ${result.path}: configs=${result.counts.configs} snapshots=${result.counts.config_snapshots} profiles=${result.counts.profiles} profile_bindings=${result.counts.profile_config_bindings} assets=${result.counts.profile_asset_bindings} machines=${result.counts.machines}`);
6927
7713
  });
6928
- program.command("import <file>").description("Import configs from a tar.gz bundle").option("--overwrite", "overwrite existing configs").action(async (file, opts) => {
7714
+ program.command("import <file>").description("Restore a v2 Instructions domain archive into an empty destination, or import legacy v1").option("--overwrite", "overwrite existing configs in a legacy v1 archive (v2 rejects this option)").action(async (file, opts) => {
6929
7715
  const result = await importConfigs(file, {
6930
7716
  conflict: opts.overwrite ? "overwrite" : "skip",
6931
7717
  store: resolveConfigStore()
6932
7718
  });
7719
+ if (result.errors.length > 0)
7720
+ throw new Error(`Import failed: ${result.errors.join("; ")}`);
6933
7721
  console.log(chalk.green("\u2713") + ` Import complete: +${result.created} updated:${result.updated} skipped:${result.skipped}`);
6934
- if (result.errors.length > 0) {
6935
- console.log(chalk.red("Errors:"));
6936
- for (const e of result.errors)
6937
- console.log(chalk.red(" " + e));
6938
- }
6939
7722
  });
6940
7723
  program.command("whoami").description("Show setup summary").action(async () => {
6941
7724
  const store = resolveConfigStore();
6942
- const dbPath = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join11(getRawStoreRoot(), "instructions.db");
7725
+ const dbPath = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join10(getRawStoreRoot(), "instructions.db");
6943
7726
  const stats = await store.getConfigStats();
6944
7727
  console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
6945
7728
  console.log(chalk.cyan(store.mode === "api" ? "API:" : "DB:") + " " + dbPath);
@@ -7647,7 +8430,7 @@ templateCmd.command("vars <id>").description("Show template variables").action(a
7647
8430
  });
7648
8431
  templateCmd.command("render <id>").description("Render a template config with variables and optionally apply to disk").option("--var <vars...>", "set variables as KEY=VALUE pairs").option("--env", "use environment variables to fill template vars").option("--apply", "write rendered output to target_path").option("--dry-run", "preview rendered output without writing").action(async (id, opts) => {
7649
8432
  try {
7650
- const { renderTemplate } = await import("../chunks/template-ke972qqe.js");
8433
+ const { renderTemplate } = await import("../chunks/template-wgnzdn7h.js");
7651
8434
  const c = await resolveConfigStore().getConfig(id);
7652
8435
  const vars = {};
7653
8436
  if (opts.var) {
@@ -7661,7 +8444,7 @@ templateCmd.command("render <id>").description("Render a template config with va
7661
8444
  }
7662
8445
  }
7663
8446
  if (opts.env) {
7664
- const { extractTemplateVars: extractTemplateVars2 } = await import("../chunks/template-ke972qqe.js");
8447
+ const { extractTemplateVars: extractTemplateVars2 } = await import("../chunks/template-wgnzdn7h.js");
7665
8448
  for (const v of extractTemplateVars2(c.content)) {
7666
8449
  if (!(v.name in vars) && process.env[v.name]) {
7667
8450
  vars[v.name] = process.env[v.name];
@@ -7710,7 +8493,7 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
7710
8493
  } else if (opts.all) {
7711
8494
  configs = await store.listConfigs(opts.category ? { kind: "file", category: opts.category } : { kind: "file" });
7712
8495
  } else {
7713
- const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await import("../chunks/sync-m46fc257.js");
8496
+ const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await import("../chunks/sync-xtkbm5br.js");
7714
8497
  const slugs = [
7715
8498
  ...KNOWN_CONFIGS2.filter((k) => !k.rulesDir).map((k) => k.name)
7716
8499
  ];
@@ -8004,7 +8787,7 @@ command = "${mcpBinary}"
8004
8787
  args = []
8005
8788
  `;
8006
8789
  if (ex(configPath)) {
8007
- const content = readFileSync8(configPath, "utf-8");
8790
+ const content = readFileSync7(configPath, "utf-8");
8008
8791
  if (findTomlTableLines(content.split(`
8009
8792
  `), "mcp_servers.configs")) {
8010
8793
  console.log(chalk.dim("= Already installed in Codex"));
@@ -8066,7 +8849,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
8066
8849
  console.log(chalk.dim("= Not installed in Codex (no config.toml)"));
8067
8850
  continue;
8068
8851
  }
8069
- const content = readFileSync8(configPath, "utf-8");
8852
+ const content = readFileSync7(configPath, "utf-8");
8070
8853
  const lines = content.split(`
8071
8854
  `);
8072
8855
  const table = findTomlTableLines(lines, "mcp_servers.configs");
@@ -8085,7 +8868,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
8085
8868
  console.log(chalk.green("\u2713") + " Removed from Codex");
8086
8869
  } else if (target === "antigravity") {
8087
8870
  const { existsSync: ex, readFileSync: rf, writeFileSync: wf } = await import("fs");
8088
- const configPath = join11(homedir4(), ".gemini", "config", "mcp_config.json");
8871
+ const configPath = join10(homedir4(), ".gemini", "config", "mcp_config.json");
8089
8872
  if (!ex(configPath)) {
8090
8873
  console.log(chalk.dim("= Not installed in Antigravity (no mcp_config.json)"));
8091
8874
  continue;
@@ -8113,6 +8896,40 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
8113
8896
  if (failed)
8114
8897
  process.exitCode = 1;
8115
8898
  });
8899
+ program.command("migrate-legacy").description("Migrate the historical ~/.hasna/configs/configs.db store into the current local Instructions SQLite store").option("--source <path>", "legacy configs.db path").option("--destination <path>", "current instructions.db path").option("--backup <path>", "exclusive destination backup path").option("--apply", "apply the migration (default is a no-write dry-run)").option("--merge-preserve-destination", "merge into a non-empty destination without overwriting existing rows").option("--confirm-local", "confirm this is an explicit on-box local-store operation").option("--json", "print a structured metadata-only result").action(async (opts) => {
8900
+ if (!selectsInstructionsLocalStore(process.env)) {
8901
+ throw new Error("migrate-legacy is local-only: set HASNA_INSTRUCTIONS_LOCAL=1 and remove every hosted Instructions authority/credential first");
8902
+ }
8903
+ if (!opts.confirmLocal) {
8904
+ throw new Error("migrate-legacy requires --confirm-local");
8905
+ }
8906
+ const destinationPath = resolve6(opts.destination || process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join10(getRawStoreRoot(), "instructions.db"));
8907
+ const database = await import("../chunks/database-wstmr56z.js");
8908
+ database.getDatabase(destinationPath);
8909
+ database.resetDatabase();
8910
+ const { migrateLegacyInstructionsStore } = await import("../chunks/legacy-store-migration-z2m5yzyk.js");
8911
+ const result = migrateLegacyInstructionsStore({
8912
+ ...opts.source ? { sourcePath: resolve6(opts.source) } : {},
8913
+ destinationPath,
8914
+ ...opts.backup ? { backupPath: resolve6(opts.backup) } : {},
8915
+ dryRun: !opts.apply,
8916
+ ...opts.mergePreserveDestination ? { mergePolicy: "preserve-destination" } : {},
8917
+ operatorConfirmed: true
8918
+ });
8919
+ if (opts.json) {
8920
+ printJson(result);
8921
+ return;
8922
+ }
8923
+ const total = Object.values(result.migrated).reduce((sum, count) => sum + count, 0);
8924
+ const skipped = Object.values(result.skipped).reduce((sum, count) => sum + count, 0);
8925
+ const mode = result.dryRun ? "Dry-run" : "Migrated";
8926
+ console.log(chalk.green("\u2713") + ` ${mode}: ${total} row(s), skipped ${skipped}, conflicts ${result.conflicts}`);
8927
+ if (result.snapshotsCreated > 0) {
8928
+ console.log(chalk.dim(` created ${result.snapshotsCreated} missing current-version snapshot(s)`));
8929
+ }
8930
+ if (result.backupCreated)
8931
+ console.log(chalk.dim(" destination backup created before mutation"));
8932
+ });
8116
8933
  program.command("init").description("First-time setup: sync all known configs, create default profile").option("--force", "delete existing DB and start fresh").action(async (opts) => {
8117
8934
  const store = resolveConfigStore();
8118
8935
  if (opts.force) {
@@ -8172,7 +8989,7 @@ DB stats:`));
8172
8989
  if (count > 0)
8173
8990
  console.log(` ${key.padEnd(18)} ${count}`);
8174
8991
  }
8175
- const location = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join11(getRawStoreRoot(), "instructions.db");
8992
+ const location = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join10(getRawStoreRoot(), "instructions.db");
8176
8993
  console.log(chalk.dim(`
8177
8994
  ${store.mode === "api" ? "API" : "DB"}: ${location}`));
8178
8995
  });
@@ -8231,24 +9048,187 @@ managedSkillsCmd.command("apply").option("--dry-run", "preview without writing")
8231
9048
  if (report.failed > 0)
8232
9049
  process.exitCode = 1;
8233
9050
  });
9051
+ function normalizeBackupVersionOptions(opts) {
9052
+ const payloadVersionId = opts.payloadVersionId?.trim();
9053
+ const manifestVersionId = opts.manifestVersionId?.trim();
9054
+ if (Boolean(payloadVersionId) !== Boolean(manifestVersionId)) {
9055
+ throw new Error("S3 backup payload and manifest version ids must be provided together");
9056
+ }
9057
+ return payloadVersionId && manifestVersionId ? { payloadVersionId, manifestVersionId } : {};
9058
+ }
9059
+ var storageCmd = program.command("storage").description("Inspect and use the optional native S3 backup plane");
9060
+ storageCmd.command("status").description("Show redacted S3 configuration status without opening SQLite or contacting S3").option("--json", "output JSON").action(async (opts) => {
9061
+ const { loadInstructionsS3Config } = await import("../chunks/s3-config-hhght91k.js");
9062
+ const config = loadInstructionsS3Config(process.env);
9063
+ const status = config ? {
9064
+ configured: true,
9065
+ provider: "s3",
9066
+ region: config.region,
9067
+ prefix: config.prefix,
9068
+ endpointConfigured: Boolean(config.endpoint),
9069
+ forcePathStyle: config.forcePathStyle,
9070
+ credentialSource: config.credentials ? "static-explicit" : "runtime-default-chain",
9071
+ includesCredentialValues: false,
9072
+ databaseAuthority: false
9073
+ } : {
9074
+ configured: false,
9075
+ provider: null,
9076
+ region: null,
9077
+ prefix: null,
9078
+ endpointConfigured: false,
9079
+ forcePathStyle: false,
9080
+ credentialSource: null,
9081
+ includesCredentialValues: false,
9082
+ databaseAuthority: false
9083
+ };
9084
+ if (opts.json)
9085
+ printJson(status);
9086
+ else if (!config)
9087
+ console.log(chalk.dim("S3 backup storage is not configured."));
9088
+ else
9089
+ console.log(chalk.green("\u2713") + ` S3 backup storage configured (${config.region}, prefix ${config.prefix}; database authority: no)`);
9090
+ });
9091
+ var storageBackupCmd = storageCmd.command("backup").description("Push, pull, and verify immutable Instructions backup objects");
9092
+ storageBackupCmd.command("push <file>").description("Upload an immutable backup archive and integrity manifest").requiredOption("--id <backup-id>", "stable immutable backup identifier").option("--content-type <type>", "payload content type", "application/gzip").option("--dry-run", "plan keys and hashes without contacting S3").option("--json", "output JSON").action(async (file, opts) => {
9093
+ const abs = resolve6(file);
9094
+ if (!existsSync11(abs) || !lstatSync4(abs).isFile())
9095
+ throw new Error(`Backup file not found: ${abs}`);
9096
+ const [{ loadInstructionsS3Config }, backup, objectStore] = await Promise.all([
9097
+ import("../chunks/s3-config-hhght91k.js"),
9098
+ import("../chunks/s3-backup-hxe3zpfz.js"),
9099
+ import("../chunks/s3-object-store-n0s82swt.js")
9100
+ ]);
9101
+ const config = loadInstructionsS3Config(process.env);
9102
+ if (!config)
9103
+ throw new Error("S3 backup storage is not configured (set HASNA_INSTRUCTIONS_S3_BUCKET)");
9104
+ const bytes = await Bun.file(abs).bytes();
9105
+ if (opts.dryRun) {
9106
+ const plan = await backup.planInstructionsBackupPush({
9107
+ store: { get: async () => {
9108
+ return;
9109
+ }, head: async () => {
9110
+ return;
9111
+ } },
9112
+ prefix: config.prefix,
9113
+ backupId: opts.id,
9114
+ bytes,
9115
+ contentType: opts.contentType
9116
+ });
9117
+ if (opts.json)
9118
+ printJson(plan);
9119
+ else
9120
+ console.log(chalk.dim(`Would upload ${plan.sizeBytes} bytes to ${plan.payloadKey} (sha256 ${plan.sha256})`));
9121
+ return;
9122
+ }
9123
+ const result = await backup.pushInstructionsBackup({
9124
+ store: objectStore.createInstructionsS3ObjectStore(config),
9125
+ prefix: config.prefix,
9126
+ backupId: opts.id,
9127
+ bytes,
9128
+ contentType: opts.contentType
9129
+ });
9130
+ const safe = {
9131
+ status: result.status,
9132
+ backupId: result.manifest.backupId,
9133
+ sha256: result.manifest.sha256,
9134
+ sizeBytes: result.manifest.sizeBytes,
9135
+ ...result.versions ? result.versions : {}
9136
+ };
9137
+ if (opts.json)
9138
+ printJson(safe);
9139
+ else
9140
+ console.log(chalk.green("\u2713") + ` S3 backup ${result.status}: ${safe.backupId} (${safe.sizeBytes} bytes, sha256 ${safe.sha256})`);
9141
+ });
9142
+ storageBackupCmd.command("verify <backup-id>").description("Verify the immutable S3 payload against its manifest").option("--payload-version-id <version-id>", "exact immutable S3 payload VersionId").option("--manifest-version-id <version-id>", "exact immutable S3 manifest VersionId").option("--json", "output JSON").action(async (backupId, opts) => {
9143
+ const versions = normalizeBackupVersionOptions(opts);
9144
+ const [{ loadInstructionsS3Config }, backup, objectStore] = await Promise.all([
9145
+ import("../chunks/s3-config-hhght91k.js"),
9146
+ import("../chunks/s3-backup-hxe3zpfz.js"),
9147
+ import("../chunks/s3-object-store-n0s82swt.js")
9148
+ ]);
9149
+ const config = loadInstructionsS3Config(process.env);
9150
+ if (!config)
9151
+ throw new Error("S3 backup storage is not configured (set HASNA_INSTRUCTIONS_S3_BUCKET)");
9152
+ const verified = await backup.verifyInstructionsBackup({
9153
+ store: objectStore.createInstructionsS3ObjectStore(config),
9154
+ prefix: config.prefix,
9155
+ backupId,
9156
+ ...versions
9157
+ });
9158
+ const safe = {
9159
+ ...verified,
9160
+ ...verified.versions ? verified.versions : {},
9161
+ versions: undefined
9162
+ };
9163
+ if (opts.json)
9164
+ printJson(safe);
9165
+ else
9166
+ console.log(chalk.green("\u2713") + ` Verified ${verified.backupId}: ${verified.sizeBytes} bytes, sha256 ${verified.sha256}${verified.versionPinned ? " (exact S3 versions)" : ""}`);
9167
+ });
9168
+ storageBackupCmd.command("pull <backup-id>").description("Download and verify an immutable S3 backup before writing it locally").requiredOption("-o, --output <path>", "destination archive path").option("--payload-version-id <version-id>", "exact immutable S3 payload VersionId").option("--manifest-version-id <version-id>", "exact immutable S3 manifest VersionId").option("--force", "replace an existing regular destination file").option("--json", "output JSON").action(async (backupId, opts) => {
9169
+ const versions = normalizeBackupVersionOptions(opts);
9170
+ const [{ loadInstructionsS3Config }, backup, objectStore] = await Promise.all([
9171
+ import("../chunks/s3-config-hhght91k.js"),
9172
+ import("../chunks/s3-backup-hxe3zpfz.js"),
9173
+ import("../chunks/s3-object-store-n0s82swt.js")
9174
+ ]);
9175
+ const config = loadInstructionsS3Config(process.env);
9176
+ if (!config)
9177
+ throw new Error("S3 backup storage is not configured (set HASNA_INSTRUCTIONS_S3_BUCKET)");
9178
+ const pulled = await backup.pullInstructionsBackup({
9179
+ store: objectStore.createInstructionsS3ObjectStore(config),
9180
+ prefix: config.prefix,
9181
+ backupId,
9182
+ ...versions
9183
+ });
9184
+ const output = resolve6(opts.output);
9185
+ if (existsSync11(output)) {
9186
+ if (!opts.force)
9187
+ throw new Error(`Refusing to overwrite existing backup file: ${output}`);
9188
+ if (!lstatSync4(output).isFile() || lstatSync4(output).isSymbolicLink()) {
9189
+ throw new Error(`Refusing to replace a non-regular backup destination: ${output}`);
9190
+ }
9191
+ }
9192
+ mkdirSync6(dirname6(output), { recursive: true, mode: 448 });
9193
+ const temp = join10(dirname6(output), `.${basename(output)}.tmp-${process.pid}-${Date.now()}`);
9194
+ try {
9195
+ writeFileSync5(temp, pulled.bytes, { flag: "wx", mode: 384 });
9196
+ renameSync2(temp, output);
9197
+ } catch (error) {
9198
+ try {
9199
+ unlinkSync(temp);
9200
+ } catch {}
9201
+ throw error;
9202
+ }
9203
+ const safe = {
9204
+ backupId: pulled.manifest.backupId,
9205
+ output,
9206
+ sha256: pulled.manifest.sha256,
9207
+ sizeBytes: pulled.manifest.sizeBytes,
9208
+ versionPinned: pulled.versionPinned,
9209
+ ...pulled.versions ? pulled.versions : {}
9210
+ };
9211
+ if (opts.json)
9212
+ printJson(safe);
9213
+ else
9214
+ console.log(chalk.green("\u2713") + ` Restored ${safe.backupId} to ${safe.output} (${safe.sizeBytes} bytes)`);
9215
+ });
8234
9216
  program.command("backup").description("Export configs to a timestamped backup file").action(async () => {
8235
9217
  const { mkdirSync: mk } = await import("fs");
8236
- const backupDir = join11(getRawStoreRoot(), "backups");
9218
+ const backupDir = join10(getRawStoreRoot(), "backups");
8237
9219
  mk(backupDir, { recursive: true });
8238
9220
  const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 19);
8239
- const outPath = join11(backupDir, `configs-${ts}.tar.gz`);
9221
+ const outPath = join10(backupDir, `configs-${ts}.tar.gz`);
8240
9222
  const result = await exportConfigs(outPath, { store: resolveConfigStore() });
8241
9223
  const { statSync: st } = await import("fs");
8242
9224
  const size = st(outPath).size;
8243
9225
  console.log(chalk.green("\u2713") + ` Backup: ${result.count} configs \u2192 ${outPath} (${(size / 1024).toFixed(1)}KB)`);
8244
9226
  });
8245
- program.command("restore <file>").description("Restore configs from a backup file").option("--overwrite", "overwrite existing configs (default: skip)").action(async (file, opts) => {
9227
+ program.command("restore <file>").description("Restore a v2 domain backup into an empty destination, or restore legacy v1 configs").option("--overwrite", "overwrite existing configs in a legacy v1 archive (v2 rejects this option)").action(async (file, opts) => {
8246
9228
  const result = await importConfigs(file, { conflict: opts.overwrite ? "overwrite" : "skip", store: resolveConfigStore() });
9229
+ if (result.errors.length > 0)
9230
+ throw new Error(`Restore failed: ${result.errors.join("; ")}`);
8247
9231
  console.log(chalk.green("\u2713") + ` Restored: +${result.created} updated:${result.updated} skipped:${result.skipped}`);
8248
- if (result.errors.length > 0) {
8249
- for (const e of result.errors)
8250
- console.log(chalk.red(" " + e));
8251
- }
8252
9232
  });
8253
9233
  program.command("doctor").description("Validate configs: syntax, permissions, missing files, secrets").action(async () => {
8254
9234
  const store = resolveConfigStore();
@@ -8264,9 +9244,9 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
8264
9244
  console.log(chalk.cyan("Known files on disk:"));
8265
9245
  for (const k of KNOWN_CONFIGS) {
8266
9246
  if (k.rulesDir) {
8267
- existsSync12(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail2(`${k.rulesDir}/ not found`);
9247
+ existsSync11(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail2(`${k.rulesDir}/ not found`);
8268
9248
  } else {
8269
- existsSync12(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail2(`${k.path} not found`);
9249
+ existsSync11(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail2(`${k.path} not found`);
8270
9250
  }
8271
9251
  }
8272
9252
  const allConfigs = await store.listConfigs();
@@ -8412,7 +9392,7 @@ program.command("watch").description("Watch known config files for changes and a
8412
9392
  const store = resolveConfigStore();
8413
9393
  const interval = Number(opts.interval);
8414
9394
  const { statSync: st } = await import("fs");
8415
- const { expandPath: expandPath2 } = await import("../chunks/apply-sfzja1f3.js");
9395
+ const { expandPath: expandPath2 } = await import("../chunks/apply-pnn7a3y7.js");
8416
9396
  console.log(chalk.bold("@hasna/instructions watch") + chalk.dim(` \u2014 polling every ${interval}ms`));
8417
9397
  console.log(chalk.dim(`Watching known config files for changes\u2026
8418
9398
  `));
@@ -8420,16 +9400,16 @@ program.command("watch").description("Watch known config files for changes and a
8420
9400
  for (const k of KNOWN_CONFIGS) {
8421
9401
  if (k.rulesDir) {
8422
9402
  const absDir = expandPath2(k.rulesDir);
8423
- if (!existsSync12(absDir))
9403
+ if (!existsSync11(absDir))
8424
9404
  continue;
8425
9405
  const { readdirSync: readdirSync3 } = await import("fs");
8426
9406
  for (const f of readdirSync3(absDir).filter((f2) => f2.endsWith(".md"))) {
8427
- const abs = join11(absDir, f);
9407
+ const abs = join10(absDir, f);
8428
9408
  mtimes.set(abs, st(abs).mtimeMs);
8429
9409
  }
8430
9410
  } else {
8431
9411
  const abs = expandPath2(k.path);
8432
- if (existsSync12(abs))
9412
+ if (existsSync11(abs))
8433
9413
  mtimes.set(abs, st(abs).mtimeMs);
8434
9414
  }
8435
9415
  }
@@ -8437,7 +9417,7 @@ program.command("watch").description("Watch known config files for changes and a
8437
9417
  const tick = async () => {
8438
9418
  let changed = 0;
8439
9419
  for (const [abs, oldMtime] of mtimes) {
8440
- if (!existsSync12(abs))
9420
+ if (!existsSync11(abs))
8441
9421
  continue;
8442
9422
  const newMtime = st(abs).mtimeMs;
8443
9423
  if (newMtime !== oldMtime) {
@@ -8449,10 +9429,10 @@ program.command("watch").description("Watch known config files for changes and a
8449
9429
  for (const k of KNOWN_CONFIGS) {
8450
9430
  if (k.rulesDir) {
8451
9431
  const absDir = expandPath2(k.rulesDir);
8452
- if (!existsSync12(absDir))
9432
+ if (!existsSync11(absDir))
8453
9433
  continue;
8454
9434
  for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
8455
- const abs = join11(absDir, f);
9435
+ const abs = join10(absDir, f);
8456
9436
  if (!mtimes.has(abs)) {
8457
9437
  mtimes.set(abs, st(abs).mtimeMs);
8458
9438
  changed++;
@@ -8460,7 +9440,7 @@ program.command("watch").description("Watch known config files for changes and a
8460
9440
  }
8461
9441
  } else {
8462
9442
  const abs = expandPath2(k.path);
8463
- if (existsSync12(abs) && !mtimes.has(abs)) {
9443
+ if (existsSync11(abs) && !mtimes.has(abs)) {
8464
9444
  mtimes.set(abs, st(abs).mtimeMs);
8465
9445
  changed++;
8466
9446
  }
@@ -8488,11 +9468,11 @@ program.command("report").description("Summary of stored configs, drift, and eco
8488
9468
  if (!c.target_path)
8489
9469
  continue;
8490
9470
  const abs = expandPath(c.target_path);
8491
- if (!existsSync12(abs)) {
9471
+ if (!existsSync11(abs)) {
8492
9472
  missing++;
8493
9473
  continue;
8494
9474
  }
8495
- const disk = readFileSync8(abs, "utf-8");
9475
+ const disk = readFileSync7(abs, "utf-8");
8496
9476
  const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(c.target_path, c.format));
8497
9477
  if (redactedDisk !== c.content)
8498
9478
  drifted++;
@@ -8559,7 +9539,7 @@ program.command("clean").description("Remove configs from DB whose target files
8559
9539
  if (!c.target_path)
8560
9540
  continue;
8561
9541
  const abs = expandPath(c.target_path);
8562
- if (!existsSync12(abs)) {
9542
+ if (!existsSync11(abs)) {
8563
9543
  if (printed < maxPrinted) {
8564
9544
  if (opts.dryRun) {
8565
9545
  console.log(chalk.yellow(" would remove:") + ` ${c.slug} ${chalk.dim(`(${truncateMiddle(c.target_path, 88)})`)}`);