@hasna/instructions 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +123 -6
  2. package/dist/chunks/{apply-ynfn1av7.js → apply-7svah2qm.js} +5 -3
  3. package/dist/chunks/{apply-sfzja1f3.js → apply-jp13sa1e.js} +6 -3
  4. package/dist/chunks/database-wstmr56z.js +23 -0
  5. package/dist/chunks/{index-85n5wnd8.js → index-1cnyjnd7.js} +5 -7
  6. package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
  7. package/dist/chunks/{index-v7dkb384.js → index-4pfq42t5.js} +129 -77
  8. package/dist/chunks/index-5d7zdftc.js +186 -0
  9. package/dist/chunks/{index-1hpwskwd.js → index-7m4j8a33.js} +205 -329
  10. package/dist/chunks/index-8rb7ng2y.js +148 -0
  11. package/dist/chunks/index-arrrx0ed.js +344 -0
  12. package/dist/chunks/{index-9p9gn7gh.js → index-bzyxvwcd.js} +5 -7
  13. package/dist/chunks/index-fpeyzn3n.js +186 -0
  14. package/dist/chunks/{index-axhwg61p.js → index-fwbs6tma.js} +15 -4
  15. package/dist/chunks/index-n3fjspr0.js +119 -0
  16. package/dist/chunks/{index-e9epbsvs.js → index-z3ma4aqk.js} +15 -4
  17. package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
  18. package/dist/chunks/{local-a9nen65z.js → local-5wvv5xv3.js} +2 -2
  19. package/dist/chunks/{local-0d8qrc92.js → local-zxa5dpf4.js} +13 -188
  20. package/dist/chunks/s3-backup-n2z4fwyh.js +240 -0
  21. package/dist/chunks/s3-config-hhght91k.js +14 -0
  22. package/dist/chunks/s3-object-store-90tgbh6k.js +14 -0
  23. package/dist/chunks/{sync-m46fc257.js → sync-2075deve.js} +5 -4
  24. package/dist/chunks/{sync-vy1yjvs6.js → sync-r8t7zsp3.js} +4 -4
  25. package/dist/chunks/{template-02wjvv05.js → template-fsfxpe4p.js} +1 -1
  26. package/dist/chunks/{template-ke972qqe.js → template-wgnzdn7h.js} +1 -1
  27. package/dist/cli/index.js +1011 -247
  28. package/dist/data/config-store.d.ts +1 -0
  29. package/dist/data/config-store.d.ts.map +1 -1
  30. package/dist/index.js +840 -215
  31. package/dist/lib/apply.d.ts +7 -0
  32. package/dist/lib/apply.d.ts.map +1 -1
  33. package/dist/lib/compact-output.d.ts +1 -0
  34. package/dist/lib/compact-output.d.ts.map +1 -1
  35. package/dist/lib/export.d.ts +22 -3
  36. package/dist/lib/export.d.ts.map +1 -1
  37. package/dist/lib/import.d.ts +18 -0
  38. package/dist/lib/import.d.ts.map +1 -1
  39. package/dist/lib/legacy-store-migration.d.ts +39 -0
  40. package/dist/lib/legacy-store-migration.d.ts.map +1 -0
  41. package/dist/lib/managed-skill-runtimes.d.ts +1 -0
  42. package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
  43. package/dist/lib/sync-dir.d.ts.map +1 -1
  44. package/dist/mcp/index.js +31 -20
  45. package/dist/sdk/index.d.ts +2 -0
  46. package/dist/sdk/index.d.ts.map +1 -1
  47. package/dist/sdk/index.js +205 -10
  48. package/dist/sdk/resolve.d.ts +1 -1
  49. package/dist/sdk/resolve.d.ts.map +1 -1
  50. package/dist/sdk/v1-client.d.ts +57 -0
  51. package/dist/sdk/v1-client.d.ts.map +1 -0
  52. package/dist/sdk/v1.generated.d.ts +212 -21
  53. package/dist/sdk/v1.generated.d.ts.map +1 -1
  54. package/dist/server/cloud.d.ts.map +1 -1
  55. package/dist/server/index.d.ts +13 -5
  56. package/dist/server/index.d.ts.map +1 -1
  57. package/dist/server/index.js +1180 -432
  58. package/dist/server/openapi.d.ts +1264 -92
  59. package/dist/server/openapi.d.ts.map +1 -1
  60. package/dist/server/v1.d.ts +25 -1
  61. package/dist/server/v1.d.ts.map +1 -1
  62. package/dist/storage/cloud-store.d.ts +57 -1
  63. package/dist/storage/cloud-store.d.ts.map +1 -1
  64. package/dist/storage/index.d.ts +7 -0
  65. package/dist/storage/index.d.ts.map +1 -0
  66. package/dist/storage/index.js +725 -0
  67. package/dist/storage/s3-backup.d.ts +76 -0
  68. package/dist/storage/s3-backup.d.ts.map +1 -0
  69. package/dist/storage/s3-config.d.ts +46 -0
  70. package/dist/storage/s3-config.d.ts.map +1 -0
  71. package/dist/storage/s3-object-store.d.ts +70 -0
  72. package/dist/storage/s3-object-store.d.ts.map +1 -0
  73. package/dist/types/index.d.ts +65 -3
  74. package/dist/types/index.d.ts.map +1 -1
  75. package/hasna.contract.json +110 -0
  76. package/migrations/0001_instructions.sql +63 -0
  77. package/migrations/0002_api_keys.sql +20 -0
  78. package/migrations/0003_idempotency_receipts.sql +24 -0
  79. package/package.json +22 -11
  80. package/assets/skills/inbox/SKILL.md +0 -86
package/dist/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-1cnyjnd7.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-z3ma4aqk.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-7m4j8a33.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,698 @@ 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].sort((left, right) => compareText(left.config_slug, right.config_slug) || left.version - right.version || compareText(left.id, right.id));
3882
+ const profiles = [...domain.profiles].sort((left, right) => compareText(left.slug, right.slug));
3883
+ 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));
3884
+ 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));
3885
+ const machines = [...domain.machines].sort((left, right) => compareText(left.hostname, right.hostname));
3886
+ const idToSlug = new Map(configs.map((config) => [config.id, config.slug]));
3887
+ const configOrder = new Map;
3888
+ const assetOrder = new Map;
3889
+ return {
3890
+ configs: configs.map((config) => ({
3891
+ slug: config.slug,
3892
+ name: config.name,
3893
+ kind: config.kind,
3894
+ category: config.category,
3895
+ agent: config.agent,
3896
+ target_path: config.target_path,
3897
+ outputs: config.outputs,
3898
+ format: config.format,
3899
+ content: config.content,
3900
+ description: config.description,
3901
+ tags: [...config.tags].sort(compareText),
3902
+ is_template: config.is_template,
3903
+ version: config.version,
3904
+ ...options.includeOperationalTimestamps ? {
3905
+ created_at: config.created_at,
3906
+ updated_at: config.updated_at,
3907
+ synced_at: config.synced_at
3908
+ } : {}
3909
+ })),
3910
+ config_snapshots: snapshots.map((snapshot) => ({
3911
+ config_slug: snapshot.config_slug,
3912
+ version: snapshot.version,
3913
+ content: snapshot.content,
3914
+ ...options.includeOperationalTimestamps ? { created_at: snapshot.created_at } : {}
3915
+ })),
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.config_slug}@${row.version}`), "config snapshot");
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 configSlugs = new Set(typed.configs.map((row) => row.slug));
4043
+ const profileSlugs = new Set(typed.profiles.map((row) => row.slug));
4044
+ for (const config of typed.configs) {
4045
+ assertTimestamp(config.created_at, `config ${config.slug} created_at`);
4046
+ assertTimestamp(config.updated_at, `config ${config.slug} updated_at`);
4047
+ assertTimestamp(config.synced_at, `config ${config.slug} synced_at`, true);
4048
+ if (!Number.isSafeInteger(config.version) || config.version < 1)
4049
+ throw new Error(`Invalid v2 archive: config ${config.slug} has invalid version`);
4050
+ const snapshots = typed.config_snapshots.filter((row) => row.config_slug === config.slug).sort((a, b) => a.version - b.version);
4051
+ if (snapshots.some((row) => !Number.isSafeInteger(row.version) || row.version < 1 || row.version > config.version)) {
4052
+ throw new Error(`Invalid v2 archive: config ${config.slug} has an invalid snapshot version`);
4053
+ }
4054
+ if (snapshots.length) {
4055
+ const first = snapshots[0].version;
4056
+ const expected = Array.from({ length: config.version - first + 1 }, (_, index) => first + index);
4057
+ if (canonicalDomainJson(snapshots.map((row) => row.version)) !== canonicalDomainJson(expected) || snapshots.at(-1).version !== config.version) {
4058
+ throw new Error(`Invalid v2 archive: snapshots for ${config.slug} must be a contiguous suffix ending at version ${config.version}`);
4059
+ }
4060
+ if (snapshots.at(-1).content !== config.content) {
4061
+ throw new Error(`Invalid v2 archive: latest snapshot content differs from config ${config.slug}`);
4062
+ }
4063
+ }
4064
+ }
4065
+ for (const row of typed.config_snapshots) {
4066
+ if (!configSlugs.has(row.config_slug))
4067
+ throw new Error(`Invalid v2 archive: snapshot references missing config ${row.config_slug}`);
4068
+ assertTimestamp(row.created_at, `snapshot ${row.config_slug}@${row.version} created_at`);
4069
+ }
4070
+ for (const profile of typed.profiles) {
4071
+ assertTimestamp(profile.created_at, `profile ${profile.slug} created_at`);
4072
+ assertTimestamp(profile.updated_at, `profile ${profile.slug} updated_at`);
4073
+ }
4074
+ for (const row of typed.profile_config_bindings) {
4075
+ if (!profileSlugs.has(row.profile_slug) || !configSlugs.has(row.config_slug)) {
4076
+ throw new Error(`Invalid v2 archive: profile config binding has a missing logical reference`);
4077
+ }
4078
+ }
4079
+ for (const row of typed.profile_asset_bindings) {
4080
+ if (!profileSlugs.has(row.profile_slug) || !configSlugs.has(row.source_config_slug)) {
4081
+ throw new Error(`Invalid v2 archive: profile asset binding has a missing logical reference`);
4082
+ }
4083
+ }
4084
+ for (const machine of typed.machines) {
4085
+ assertTimestamp(machine.created_at, `machine ${machine.hostname} created_at`);
4086
+ assertTimestamp(machine.last_applied_at, `machine ${machine.hostname} last_applied_at`, true);
4087
+ }
4088
+ return typed;
4089
+ }
4090
+ function matchesFilter(config, filter) {
4091
+ if (!filter)
4092
+ return true;
4093
+ if (filter.category && config.category !== filter.category)
4094
+ return false;
4095
+ if (filter.agent && config.agent !== filter.agent)
4096
+ return false;
4097
+ if (filter.kind && config.kind !== filter.kind)
4098
+ return false;
4099
+ if (filter.is_template !== undefined && config.is_template !== filter.is_template)
4100
+ return false;
4101
+ if (filter.tags?.length && !filter.tags.every((tag) => config.tags.includes(tag)))
4102
+ return false;
4103
+ if (filter.search) {
4104
+ const needle = filter.search.toLocaleLowerCase();
4105
+ if (![config.name, config.description ?? "", config.content].some((value) => value.toLocaleLowerCase().includes(needle)))
4106
+ return false;
4107
+ }
4108
+ return true;
4109
+ }
4110
+ async function collectInstructionsDomain(opts = {}) {
3827
4111
  const store = opts.store ?? resolveConfigStore();
3828
- const configs = await store.listConfigs(opts.filter);
4112
+ const selectedProfile = opts.profileId ? await store.getProfile(opts.profileId) : null;
4113
+ const listedConfigs = selectedProfile ? await store.getProfileConfigs(selectedProfile.id) : await store.listConfigs(opts.filter);
4114
+ const configs = listedConfigs.filter((config) => matchesFilter(config, selectedProfile ? opts.filter : undefined));
4115
+ configs.sort((left, right) => compareText(left.slug, right.slug));
4116
+ const selectedIds = new Set(configs.map((config) => config.id));
4117
+ const profiles = (selectedProfile ? [selectedProfile] : await store.listProfiles()).sort((left, right) => compareText(left.slug, right.slug));
4118
+ const profileById = new Map(profiles.map((profile) => [profile.id, profile]));
4119
+ const configById = new Map(configs.map((config) => [config.id, config]));
4120
+ const configSnapshots = [];
4121
+ for (const config of configs) {
4122
+ for (const snapshot of await store.listSnapshots(config.id)) {
4123
+ configSnapshots.push({
4124
+ id: snapshot.id,
4125
+ config_slug: config.slug,
4126
+ content: snapshot.content,
4127
+ version: snapshot.version,
4128
+ created_at: snapshot.created_at
4129
+ });
4130
+ }
4131
+ }
4132
+ configSnapshots.sort((left, right) => compareText(left.config_slug, right.config_slug) || left.version - right.version || compareText(left.id, right.id));
4133
+ const profileConfigBindings = (await Promise.all(profiles.map(async (profile) => (await store.getProfileConfigBindings(profile.id)).filter((row) => selectedIds.has(row.config_id)).map((row) => ({
4134
+ profile_slug: profile.slug,
4135
+ config_slug: configById.get(row.config_id).slug,
4136
+ sort_order: row.sort_order,
4137
+ binding: row.binding
4138
+ }))))).flat().sort((left, right) => compareText(left.profile_slug, right.profile_slug) || left.sort_order - right.sort_order || compareText(left.config_slug, right.config_slug));
4139
+ const profileAssetBindings = (await Promise.all(profiles.map(async (profile) => (await store.getProfileAssetBindings(profile.id)).filter((row) => selectedIds.has(row.source_config_id)).map((row) => ({
4140
+ profile_slug: profile.slug,
4141
+ source_config_slug: configById.get(row.source_config_id).slug,
4142
+ sort_order: row.sort_order,
4143
+ binding: row.binding
4144
+ }))))).flat().sort((left, right) => compareText(left.profile_slug, right.profile_slug) || left.sort_order - right.sort_order || compareText(left.binding.assetKey, right.binding.assetKey));
4145
+ const archivedProfiles = profiles.filter((profile) => profileById.has(profile.id));
4146
+ const machines = (await store.listMachines()).sort((left, right) => compareText(left.hostname, right.hostname));
4147
+ return {
4148
+ schema: INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
4149
+ configs,
4150
+ config_snapshots: configSnapshots,
4151
+ profiles: archivedProfiles,
4152
+ profile_config_bindings: profileConfigBindings,
4153
+ profile_asset_bindings: profileAssetBindings,
4154
+ machines
4155
+ };
4156
+ }
4157
+ async function exportConfigs(outputPath, opts = {}) {
4158
+ const domain = validateInstructionsDomainArchive(await collectInstructionsDomain(opts));
4159
+ const integrity = computeDomainIntegrity(domain);
4160
+ const payload = `${JSON.stringify(domain, null, 2)}
4161
+ `;
4162
+ const manifest = {
4163
+ schema: INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
4164
+ version: "2.0.0",
4165
+ exported_at: new Date().toISOString(),
4166
+ payload: {
4167
+ path: "domain.json",
4168
+ sha256: sha256(payload),
4169
+ size_bytes: Buffer.byteLength(payload)
4170
+ },
4171
+ integrity,
4172
+ exclusions: ARCHIVE_EXCLUSIONS
4173
+ };
3829
4174
  const absOutput = resolve2(outputPath);
3830
- const tmpDir = join3(tmpdir(), `configs-export-${Date.now()}`);
3831
- const contentsDir = join3(tmpDir, "contents");
4175
+ mkdirSync(dirname(absOutput), { recursive: true });
4176
+ const stagingDir = mkdtempSync(join3(tmpdir(), "instructions-domain-export-"));
3832
4177
  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, "."], {
4178
+ writeFileSync(join3(stagingDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
4179
+ `, "utf-8");
4180
+ writeFileSync(join3(stagingDir, "domain.json"), payload, "utf-8");
4181
+ const proc = Bun.spawn(["tar", "czf", absOutput, "-C", stagingDir, "manifest.json", "domain.json"], {
3845
4182
  stdout: "pipe",
3846
4183
  stderr: "pipe"
3847
4184
  });
3848
4185
  const exitCode = await proc.exited;
3849
4186
  if (exitCode !== 0) {
3850
4187
  const stderr = await new Response(proc.stderr).text();
3851
- throw new Error(`tar failed: ${stderr}`);
4188
+ throw new Error(`tar failed: ${stderr.trim()}`);
3852
4189
  }
3853
- return { path: absOutput, count: configs.length };
4190
+ return { path: absOutput, count: domain.configs.length, counts: integrity.counts, integrity };
3854
4191
  } finally {
3855
- if (existsSync3(tmpDir)) {
3856
- rmSync(tmpDir, { recursive: true, force: true });
3857
- }
4192
+ if (existsSync3(stagingDir))
4193
+ rmSync(stagingDir, { recursive: true, force: true });
3858
4194
  }
3859
4195
  }
3860
4196
 
3861
4197
  // 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: [] };
4198
+ import { createHash as createHash2 } from "crypto";
4199
+ import { resolve as resolve3 } from "path";
4200
+ function compareText2(left, right) {
4201
+ return left < right ? -1 : left > right ? 1 : 0;
4202
+ }
4203
+ function emptyMutationCounts() {
4204
+ return { created: 0, updated: 0, skipped: 0 };
4205
+ }
4206
+ function emptyResult() {
4207
+ return {
4208
+ created: 0,
4209
+ updated: 0,
4210
+ skipped: 0,
4211
+ counts: {
4212
+ configs: emptyMutationCounts(),
4213
+ config_snapshots: { created: 0, skipped: 0 },
4214
+ profiles: emptyMutationCounts(),
4215
+ profile_config_bindings: emptyMutationCounts(),
4216
+ profile_asset_bindings: emptyMutationCounts(),
4217
+ machines: emptyMutationCounts()
4218
+ },
4219
+ errors: [],
4220
+ integrity: null
4221
+ };
4222
+ }
4223
+ function sha2562(value) {
4224
+ return createHash2("sha256").update(value).digest("hex");
4225
+ }
4226
+ function normalizeMemberName(name) {
4227
+ let normalized = name;
4228
+ while (normalized.startsWith("./"))
4229
+ normalized = normalized.slice(2);
4230
+ if (normalized.startsWith("/") || normalized.includes("\x00") || normalized.split("/").some((segment) => segment === "..")) {
4231
+ throw new Error(`Invalid archive member path: ${JSON.stringify(name)}`);
4232
+ }
4233
+ return normalized;
4234
+ }
4235
+ async function openArchive(path) {
4236
+ const list = Bun.spawn(["tar", "tzf", path], { stdout: "pipe", stderr: "pipe" });
4237
+ const [exitCode, stdout, stderr] = await Promise.all([
4238
+ list.exited,
4239
+ new Response(list.stdout).text(),
4240
+ new Response(list.stderr).text()
4241
+ ]);
4242
+ if (exitCode !== 0)
4243
+ throw new Error(`tar listing failed: ${stderr.trim()}`);
4244
+ const members = new Map;
4245
+ for (const raw of stdout.split(`
4246
+ `).filter(Boolean)) {
4247
+ const normalized = normalizeMemberName(raw);
4248
+ if (normalized.length === 0)
4249
+ continue;
4250
+ if (members.has(normalized))
4251
+ throw new Error(`Invalid archive: duplicate member ${normalized}`);
4252
+ members.set(normalized, raw);
4253
+ }
4254
+ return {
4255
+ members,
4256
+ async read(member) {
4257
+ const actual = members.get(member);
4258
+ if (!actual)
4259
+ throw new Error(`Invalid archive: missing ${member}`);
4260
+ const proc = Bun.spawn(["tar", "xOzf", path, "--", actual], { stdout: "pipe", stderr: "pipe" });
4261
+ const [code, content, memberError] = await Promise.all([
4262
+ proc.exited,
4263
+ new Response(proc.stdout).text(),
4264
+ new Response(proc.stderr).text()
4265
+ ]);
4266
+ if (code !== 0)
4267
+ throw new Error(`tar read failed for ${member}: ${memberError.trim()}`);
4268
+ return content;
4269
+ }
4270
+ };
4271
+ }
4272
+ function isNotFound(error) {
4273
+ return Boolean(error && typeof error === "object" && (error.name === "ConfigNotFoundError" || error.name === "ProfileNotFoundError" || error.status === 404));
4274
+ }
4275
+ async function findConfig(store, slug) {
3871
4276
  try {
3872
- mkdirSync2(tmpDir, { recursive: true });
3873
- const proc = Bun.spawn(["tar", "xzf", absPath, "-C", tmpDir], {
3874
- stdout: "pipe",
3875
- stderr: "pipe"
4277
+ return await store.getConfig(slug);
4278
+ } catch (error) {
4279
+ if (isNotFound(error))
4280
+ return null;
4281
+ throw error;
4282
+ }
4283
+ }
4284
+ function verifyCounts(actual, expected) {
4285
+ if (canonicalDomainJson(actual) !== canonicalDomainJson(expected)) {
4286
+ throw new Error("Archive integrity failure: manifest counts do not match the domain payload");
4287
+ }
4288
+ }
4289
+ async function readV2(reader, manifest) {
4290
+ 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") {
4291
+ throw new Error("Invalid v2 archive manifest");
4292
+ }
4293
+ const payload = await reader.read("domain.json");
4294
+ if (Buffer.byteLength(payload) !== manifest.payload.size_bytes || sha2562(payload) !== manifest.payload.sha256) {
4295
+ throw new Error("Archive integrity failure: domain payload hash or size does not match the manifest");
4296
+ }
4297
+ const domain = validateInstructionsDomainArchive(JSON.parse(payload));
4298
+ const actual = computeDomainIntegrity(domain);
4299
+ verifyCounts(actual.counts, manifest.integrity.counts);
4300
+ if (canonicalDomainJson(actual) !== canonicalDomainJson(manifest.integrity)) {
4301
+ throw new Error("Archive integrity failure: logical domain hashes do not match the manifest");
4302
+ }
4303
+ return domain;
4304
+ }
4305
+ function configInput(config, content) {
4306
+ return {
4307
+ name: config.name,
4308
+ kind: config.kind,
4309
+ category: config.category,
4310
+ agent: config.agent,
4311
+ target_path: config.target_path,
4312
+ outputs: config.outputs,
4313
+ format: config.format,
4314
+ content,
4315
+ description: config.description ?? undefined,
4316
+ tags: config.tags,
4317
+ is_template: config.is_template
4318
+ };
4319
+ }
4320
+ async function restoreNewConfig(store, archived, domain) {
4321
+ const snapshots = domain.config_snapshots.filter((row) => row.config_slug === archived.slug).sort((left, right) => left.version - right.version);
4322
+ const byVersion = new Map(snapshots.map((snapshot) => [snapshot.version, snapshot.content]));
4323
+ let content = byVersion.get(1) ?? snapshots[0]?.content ?? archived.content;
4324
+ let restored = await store.createConfig(configInput(archived, content));
4325
+ for (let version = 2;version <= archived.version; version++) {
4326
+ content = byVersion.get(version) ?? (version === archived.version ? archived.content : content);
4327
+ restored = await store.updateConfig(restored.id, {
4328
+ content,
4329
+ ...version === archived.version ? {
4330
+ name: archived.name,
4331
+ kind: archived.kind,
4332
+ category: archived.category,
4333
+ agent: archived.agent,
4334
+ target_path: archived.target_path,
4335
+ outputs: archived.outputs,
4336
+ format: archived.format,
4337
+ description: archived.description,
4338
+ tags: archived.tags,
4339
+ is_template: archived.is_template,
4340
+ synced_at: archived.synced_at
4341
+ } : {}
3876
4342
  });
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}`);
4343
+ }
4344
+ await store.pruneSnapshots(restored.id, snapshots.length);
4345
+ const restoredSnapshots = (await store.listSnapshots(restored.id)).map(({ version, content: snapshotContent }) => ({ version, content: snapshotContent })).sort((left, right) => left.version - right.version);
4346
+ const expectedSnapshots = snapshots.map(({ version, content: snapshotContent }) => ({ version, content: snapshotContent }));
4347
+ if (restored.version !== archived.version || restored.content !== archived.content || canonicalDomainJson(restoredSnapshots) !== canonicalDomainJson(expectedSnapshots)) {
4348
+ throw new Error(`restored config or snapshot history did not match archive for ${archived.slug}`);
4349
+ }
4350
+ return restored;
4351
+ }
4352
+ function rewriteAssetLocator(binding, archivedSourceId, restoredSourceId) {
4353
+ const match = /^config:\/\/([^@]+)@(\d+)$/.exec(binding.source.locator);
4354
+ if (!match)
4355
+ return binding;
4356
+ let locatorId;
4357
+ try {
4358
+ locatorId = decodeURIComponent(match[1]);
4359
+ } catch {
4360
+ return binding;
4361
+ }
4362
+ if (locatorId !== archivedSourceId)
4363
+ return binding;
4364
+ return {
4365
+ ...binding,
4366
+ source: {
4367
+ ...binding.source,
4368
+ locator: `config://${encodeURIComponent(restoredSourceId)}@${match[2]}`
3881
4369
  }
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)}`);
4370
+ };
4371
+ }
4372
+ async function assertEmptyV2Destination(store) {
4373
+ const [configs, profiles, machines] = await Promise.all([
4374
+ store.listConfigs(),
4375
+ store.listProfiles(),
4376
+ store.listMachines()
4377
+ ]);
4378
+ if (configs.length > 0 || profiles.length > 0 || machines.length > 0) {
4379
+ throw new Error(`Instructions domain archive v2 recovery requires an empty destination ` + `(found configs=${configs.length}, profiles=${profiles.length}, machines=${machines.length})`);
4380
+ }
4381
+ }
4382
+ async function importV2(domain, manifest, store, conflict) {
4383
+ if (conflict !== "skip") {
4384
+ throw new Error(`Instructions domain archive v2 does not support ${conflict}; restore into an empty destination`);
4385
+ }
4386
+ await assertEmptyV2Destination(store);
4387
+ const result = emptyResult();
4388
+ result.integrity = manifest.integrity;
4389
+ const configMap = new Map;
4390
+ for (const archived of domain.configs) {
4391
+ const restored = await restoreNewConfig(store, archived, domain);
4392
+ configMap.set(archived.slug, restored);
4393
+ result.counts.configs.created++;
4394
+ result.created++;
4395
+ result.counts.config_snapshots.created += domain.config_snapshots.filter((row) => row.config_slug === archived.slug).length;
4396
+ }
4397
+ const profileMap = new Map;
4398
+ for (const archived of domain.profiles) {
4399
+ const created = await store.createProfile({
4400
+ name: archived.name,
4401
+ description: archived.description ?? undefined,
4402
+ selectors: archived.selectors,
4403
+ variables: archived.variables
4404
+ });
4405
+ profileMap.set(archived.slug, created);
4406
+ result.counts.profiles.created++;
4407
+ }
4408
+ for (const archived of domain.profiles) {
4409
+ const restoredProfile = profileMap.get(archived.slug);
4410
+ if (!restoredProfile)
4411
+ throw new Error(`post-create profile mapping missing for ${archived.slug}`);
4412
+ 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));
4413
+ 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));
4414
+ for (const row of archivedMembership) {
4415
+ const config = configMap.get(row.config_slug);
4416
+ if (!config)
4417
+ throw new Error(`missing restored config ${row.config_slug}`);
4418
+ await store.addConfigToProfile(restoredProfile.id, config.id);
4419
+ await store.setProfileConfigBinding(restoredProfile.id, config.id, row.binding);
4420
+ result.counts.profile_config_bindings.created++;
4421
+ }
4422
+ for (const row of archivedAssets) {
4423
+ const source = configMap.get(row.source_config_slug);
4424
+ const archivedSource = domain.configs.find((config) => config.slug === row.source_config_slug);
4425
+ if (!source || !archivedSource)
4426
+ throw new Error(`missing restored asset source ${row.source_config_slug}`);
4427
+ await store.addAssetToProfile(restoredProfile.id, source.id, rewriteAssetLocator(row.binding, archivedSource.id, source.id));
4428
+ result.counts.profile_asset_bindings.created++;
4429
+ }
4430
+ }
4431
+ for (const archived of domain.machines) {
4432
+ const restored = await store.registerMachine(archived.hostname, archived.os ?? undefined, archived.arch ?? undefined);
4433
+ if (archived.last_applied_at !== null)
4434
+ await store.updateMachineApplied(restored.hostname);
4435
+ result.counts.machines.created++;
4436
+ }
4437
+ const restoredDomain = await collectInstructionsDomain({ store });
4438
+ const expected = computeRestorableDomainIntegrity(domain);
4439
+ const actual = computeRestorableDomainIntegrity(restoredDomain);
4440
+ if (canonicalDomainJson(actual) !== canonicalDomainJson(expected)) {
4441
+ throw new Error(`Post-restore logical integrity verification failed: expected ${expected.domain_sha256}, got ${actual.domain_sha256}`);
4442
+ }
4443
+ return result;
4444
+ }
4445
+ async function importV1(reader, manifest, store, conflict) {
4446
+ const result = emptyResult();
4447
+ if (!Array.isArray(manifest.configs))
4448
+ throw new Error("Invalid v1 bundle: configs must be an array");
4449
+ for (const meta of manifest.configs) {
4450
+ const ext = meta.format === "text" ? "txt" : meta.format;
4451
+ const contentPath = `contents/${meta.slug}.${ext}`;
4452
+ const content = reader.members.has(contentPath) ? await reader.read(contentPath) : "";
4453
+ const existing = await findConfig(store, meta.slug);
4454
+ if (existing) {
4455
+ if (conflict === "skip") {
4456
+ result.skipped++;
4457
+ result.counts.configs.skipped++;
4458
+ } else {
4459
+ await store.updateConfig(existing.id, {
4460
+ name: meta.name,
4461
+ kind: meta.kind,
4462
+ category: meta.category,
4463
+ agent: meta.agent,
4464
+ target_path: meta.target_path,
4465
+ outputs: meta.outputs,
4466
+ format: meta.format,
4467
+ content,
4468
+ description: meta.description,
4469
+ tags: meta.tags,
4470
+ is_template: meta.is_template,
4471
+ synced_at: meta.synced_at
4472
+ });
4473
+ result.updated++;
4474
+ result.counts.configs.updated++;
3920
4475
  }
3921
- }
3922
- return result;
3923
- } finally {
3924
- if (existsSync5(tmpDir)) {
3925
- rmSync2(tmpDir, { recursive: true, force: true });
4476
+ } else {
4477
+ await store.createConfig(configInput({ ...meta, content }, content));
4478
+ result.created++;
4479
+ result.counts.configs.created++;
3926
4480
  }
3927
4481
  }
4482
+ return result;
4483
+ }
4484
+ async function importConfigs(bundlePath, opts = {}) {
4485
+ const store = opts.store ?? resolveConfigStore();
4486
+ const conflict = opts.conflict ?? "skip";
4487
+ const reader = await openArchive(resolve3(bundlePath));
4488
+ const manifestText = await reader.read("manifest.json");
4489
+ let manifest;
4490
+ try {
4491
+ manifest = JSON.parse(manifestText);
4492
+ } catch {
4493
+ throw new Error("Invalid bundle: manifest.json is not valid JSON");
4494
+ }
4495
+ if (!manifest || typeof manifest !== "object" || typeof manifest.version !== "string") {
4496
+ throw new Error("Invalid bundle: manifest version is missing");
4497
+ }
4498
+ if (manifest.version === "2.0.0") {
4499
+ const v2 = manifest;
4500
+ const domain = await readV2(reader, v2);
4501
+ return importV2(domain, v2, store, conflict);
4502
+ }
4503
+ if (manifest.version.startsWith("1.")) {
4504
+ return importV1(reader, manifest, store, conflict);
4505
+ }
4506
+ throw new Error(`Unsupported Instructions archive version: ${manifest.version}`);
3928
4507
  }
3929
4508
 
3930
4509
  // src/lib/session-apply.ts
3931
- import { createHash, randomUUID as randomUUID3 } from "crypto";
4510
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
3932
4511
  import {
3933
- existsSync as existsSync6,
4512
+ existsSync as existsSync5,
3934
4513
  lstatSync,
3935
- mkdirSync as mkdirSync3,
3936
- readFileSync as readFileSync2,
4514
+ mkdirSync as mkdirSync2,
4515
+ readFileSync,
3937
4516
  readdirSync,
3938
4517
  statSync
3939
4518
  } from "fs";
3940
- import { dirname, isAbsolute, join as join6, parse, relative, resolve as resolve4 } from "path";
4519
+ import { dirname as dirname2, isAbsolute, join as join5, parse, relative, resolve as resolve4 } from "path";
3941
4520
  class SessionApplyError extends Error {
3942
4521
  constructor(message) {
3943
4522
  super(message);
@@ -4068,8 +4647,8 @@ function assertClaudeAuthorityStillClear(plan, targetHome, ownedClaudeAuthoritie
4068
4647
  throw new SessionApplyError(`Claude authority changed after planning; refusing to apply: ${summary}`);
4069
4648
  }
4070
4649
  function ensureSessionTargetHome(targetHome) {
4071
- if (!existsSync6(targetHome))
4072
- mkdirSync3(targetHome, { recursive: true, mode: 448 });
4650
+ if (!existsSync5(targetHome))
4651
+ mkdirSync2(targetHome, { recursive: true, mode: 448 });
4073
4652
  assertSafeTargetHome(targetHome);
4074
4653
  }
4075
4654
  function checkSessionRenderDrift(targetHome, manifestPath) {
@@ -4091,7 +4670,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
4091
4670
  const drifted = [];
4092
4671
  for (const file of previousManifest.files) {
4093
4672
  const target = resolveManifestRelativePath(file.relativePath, safeTargetHome);
4094
- if (!existsSync6(target)) {
4673
+ if (!existsSync5(target)) {
4095
4674
  missing.push({
4096
4675
  path: target,
4097
4676
  relativePath: file.relativePath,
@@ -4101,7 +4680,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
4101
4680
  });
4102
4681
  continue;
4103
4682
  }
4104
- const actualSha256 = sha256(readFileSync2(target, "utf-8"));
4683
+ const actualSha256 = sha2563(readFileSync(target, "utf-8"));
4105
4684
  if (actualSha256 !== file.sha256) {
4106
4685
  drifted.push({
4107
4686
  path: target,
@@ -4248,7 +4827,7 @@ function requiredRestoreHash(file) {
4248
4827
  }
4249
4828
  function readSessionRenderSnapshot(snapshotPath) {
4250
4829
  const resolved = resolve4(snapshotPath);
4251
- if (!existsSync6(resolved))
4830
+ if (!existsSync5(resolved))
4252
4831
  throw new SessionApplyError(`Session snapshot not found: ${snapshotPath}`);
4253
4832
  const stat = lstatSync(resolved);
4254
4833
  if (stat.isSymbolicLink() || !stat.isFile()) {
@@ -4259,7 +4838,7 @@ function readSessionRenderSnapshot(snapshotPath) {
4259
4838
  }
4260
4839
  let parsed;
4261
4840
  try {
4262
- parsed = JSON.parse(readFileSync2(resolved, "utf8"));
4841
+ parsed = JSON.parse(readFileSync(resolved, "utf8"));
4263
4842
  } catch {
4264
4843
  throw new SessionApplyError(`Session snapshot is not valid JSON: ${snapshotPath}`);
4265
4844
  }
@@ -4281,7 +4860,7 @@ function readSessionRenderSnapshot(snapshotPath) {
4281
4860
  const previousManifest = snapshot.previousManifest;
4282
4861
  const previousFiles = new Map;
4283
4862
  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) {
4863
+ if (!file || typeof file.relativePath !== "string" || typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.content !== "string" || sha2563(file.content) !== file.sha256) {
4285
4864
  throw new SessionApplyError(`Session snapshot previous file metadata is invalid: ${snapshotPath}`);
4286
4865
  }
4287
4866
  resolveSnapshotFilePath(file.relativePath, file.path, targetHome);
@@ -4337,7 +4916,7 @@ function reconstructPreRollbackLegacyV1Snapshot(snapshot, previousFiles, previou
4337
4916
  }
4338
4917
  let parsedManifest;
4339
4918
  try {
4340
- parsedManifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
4919
+ parsedManifest = JSON.parse(readFileSync(manifestPath, "utf8"));
4341
4920
  } catch {
4342
4921
  throw new SessionApplyError(`Pre-rollback legacy v1 applied manifest is not valid JSON: ${snapshotPath}`);
4343
4922
  }
@@ -4429,15 +5008,15 @@ function assertNoNewerSessionSnapshot(snapshotPath, createdAt, targetHome) {
4429
5008
  if (!Number.isFinite(createdAtMs)) {
4430
5009
  throw new SessionApplyError(`Pre-rollback legacy v1 snapshot has an invalid creation time: ${snapshotPath}`);
4431
5010
  }
4432
- for (const entry of readdirSync(dirname(snapshotPath))) {
4433
- const candidatePath = resolve4(dirname(snapshotPath), entry);
5011
+ for (const entry of readdirSync(dirname2(snapshotPath))) {
5012
+ const candidatePath = resolve4(dirname2(snapshotPath), entry);
4434
5013
  if (candidatePath === resolve4(snapshotPath) || !entry.endsWith(".json"))
4435
5014
  continue;
4436
5015
  const candidateStat = lstatSync(candidatePath);
4437
5016
  if (candidateStat.isSymbolicLink() || !candidateStat.isFile() || candidateStat.size > 32 * 1024 * 1024)
4438
5017
  continue;
4439
5018
  try {
4440
- const candidate = JSON.parse(readFileSync2(candidatePath, "utf8"));
5019
+ const candidate = JSON.parse(readFileSync(candidatePath, "utf8"));
4441
5020
  const candidateCreatedAtMs = typeof candidate.createdAt === "string" ? Date.parse(candidate.createdAt) : Number.NaN;
4442
5021
  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
5022
  throw new SessionApplyError(`Cannot restore pre-rollback legacy v1 snapshot after a newer session snapshot exists: ${candidatePath}`);
@@ -4497,7 +5076,7 @@ function inferLegacySnapshotAction(file, previousFiles, previousManifestFiles, p
4497
5076
  return "create";
4498
5077
  }
4499
5078
  if (file.role === "manifest" && previousManifest) {
4500
- const previousManifestSha256 = sha256(`${JSON.stringify(previousManifest, null, 2)}
5079
+ const previousManifestSha256 = sha2563(`${JSON.stringify(previousManifest, null, 2)}
4501
5080
  `);
4502
5081
  if (previousManifestSha256 !== file.sha256) {
4503
5082
  throw new SessionApplyError(`Cannot infer legacy v1 manifest action without a before-image: ${file.relativePath}`);
@@ -4515,8 +5094,8 @@ function resolveSnapshotFilePath(relativePath, recordedPath, targetHome) {
4515
5094
  }
4516
5095
  function planFileResult(plan, file, targetHome, previousHashes, previousManifest, options) {
4517
5096
  const target = resolvePlannedFilePath(plan, file, targetHome);
4518
- const previousContent = existsSync6(target) ? readFileSync2(target, "utf-8") : null;
4519
- const previousSha256 = previousContent === null ? null : sha256(previousContent);
5097
+ const previousContent = existsSync5(target) ? readFileSync(target, "utf-8") : null;
5098
+ const previousSha256 = previousContent === null ? null : sha2563(previousContent);
4520
5099
  const previouslyManaged = isPreviouslyManaged(file, previousSha256, previousHashes, previousManifest);
4521
5100
  const changed = previousContent !== file.content;
4522
5101
  if (previousContent !== null && !options.force && !previouslyManaged) {
@@ -4614,10 +5193,10 @@ function planStaleFileResults(plan, targetHome, previousManifest, currentRelativ
4614
5193
  }
4615
5194
  function planStaleFileResult(file, targetHome, options) {
4616
5195
  const target = resolveManifestRelativePath(file.relativePath, targetHome);
4617
- if (!existsSync6(target))
5196
+ if (!existsSync5(target))
4618
5197
  return null;
4619
- const previousContent = readFileSync2(target, "utf-8");
4620
- const previousSha256 = sha256(previousContent);
5198
+ const previousContent = readFileSync(target, "utf-8");
5199
+ const previousSha256 = sha2563(previousContent);
4621
5200
  if (!options.force && previousSha256 !== file.sha256) {
4622
5201
  return {
4623
5202
  path: target,
@@ -4682,10 +5261,10 @@ function resolveManifestRelativePath(relativePath, targetHome) {
4682
5261
  return target;
4683
5262
  }
4684
5263
  function readPreviousManifest(path) {
4685
- if (!existsSync6(path))
5264
+ if (!existsSync5(path))
4686
5265
  return null;
4687
5266
  try {
4688
- const parsed = JSON.parse(readFileSync2(path, "utf-8"));
5267
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
4689
5268
  if (parsed.schema !== SESSION_RENDER_SCHEMA)
4690
5269
  return null;
4691
5270
  if (!Array.isArray(parsed.files))
@@ -4724,13 +5303,13 @@ function assertExpectedSessionFileHash(path, targetHome, expectedHash) {
4724
5303
  }
4725
5304
  function currentSessionFileHash(path, targetHome) {
4726
5305
  assertNoSymlinkSegments(targetHome, path);
4727
- if (!existsSync6(path))
5306
+ if (!existsSync5(path))
4728
5307
  return null;
4729
5308
  const stat = lstatSync(path);
4730
5309
  if (stat.isSymbolicLink() || !stat.isFile()) {
4731
5310
  throw new SessionApplyError(`Session apply path is not a regular file: ${path}`);
4732
5311
  }
4733
- return sha256(readFileSync2(path, "utf-8"));
5312
+ return sha2563(readFileSync(path, "utf-8"));
4734
5313
  }
4735
5314
  function requiredPreviousHash(result) {
4736
5315
  if (result.previousSha256 === null) {
@@ -4739,13 +5318,13 @@ function requiredPreviousHash(result) {
4739
5318
  return result.previousSha256;
4740
5319
  }
4741
5320
  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");
5321
+ const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").filter((result) => existsSync5(result.path)).map((result) => {
5322
+ const content = readFileSync(result.path, "utf-8");
4744
5323
  return {
4745
5324
  path: result.path,
4746
5325
  relativePath: result.relativePath,
4747
5326
  role: result.role,
4748
- sha256: sha256(content),
5327
+ sha256: sha2563(content),
4749
5328
  content
4750
5329
  };
4751
5330
  });
@@ -4758,7 +5337,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
4758
5337
  };
4759
5338
  }
4760
5339
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
4761
- const snapshotPath = join6(getSessionRenderSnapshotDir(targetHome), `${timestamp}-${randomUUID3()}.json`);
5340
+ const snapshotPath = join5(getSessionRenderSnapshotDir(targetHome), `${timestamp}-${randomUUID3()}.json`);
4762
5341
  const afterFiles = results.map((result) => {
4763
5342
  if (result.action === "conflict") {
4764
5343
  throw new SessionApplyError(`Cannot snapshot unresolved conflict: ${result.relativePath}`);
@@ -4811,7 +5390,7 @@ function assertSafeTargetHome(targetHome) {
4811
5390
  throw new SessionApplyError(`Session target home cannot be the filesystem root: ${targetHome}`);
4812
5391
  }
4813
5392
  assertNoSymlinkAncestors(normalized);
4814
- if (existsSync6(normalized) && lstatSync(normalized).isSymbolicLink()) {
5393
+ if (existsSync5(normalized) && lstatSync(normalized).isSymbolicLink()) {
4815
5394
  throw new SessionApplyError(`Session target home cannot be a symlink: ${normalized}`);
4816
5395
  }
4817
5396
  return normalized;
@@ -4821,8 +5400,8 @@ function assertNoSymlinkSegments(root, target) {
4821
5400
  const rel = relative(root, target);
4822
5401
  let current = root;
4823
5402
  for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
4824
- current = join6(current, segment);
4825
- if (existsSync6(current) && lstatSync(current).isSymbolicLink()) {
5403
+ current = join5(current, segment);
5404
+ if (existsSync5(current) && lstatSync(current).isSymbolicLink()) {
4826
5405
  throw new SessionApplyError(`Session apply path uses a symlink: ${current}`);
4827
5406
  }
4828
5407
  }
@@ -4833,16 +5412,16 @@ function assertNoSymlinkAncestors(path) {
4833
5412
  let current = parsed.root;
4834
5413
  const rel = relative(parsed.root, normalized);
4835
5414
  for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
4836
- current = join6(current, segment);
4837
- if (!existsSync6(current))
5415
+ current = join5(current, segment);
5416
+ if (!existsSync5(current))
4838
5417
  return;
4839
5418
  if (lstatSync(current).isSymbolicLink()) {
4840
5419
  throw new SessionApplyError(`Session apply path uses a symlink ancestor: ${current}`);
4841
5420
  }
4842
5421
  }
4843
5422
  }
4844
- function sha256(content) {
4845
- return createHash("sha256").update(content).digest("hex");
5423
+ function sha2563(content) {
5424
+ return createHash3("sha256").update(content).digest("hex");
4846
5425
  }
4847
5426
 
4848
5427
  // src/lib/global-source-coverage.ts
@@ -4878,9 +5457,9 @@ function formatGlobalSourceCoverageWarnings(result) {
4878
5457
 
4879
5458
  // src/lib/station-profile.ts
4880
5459
  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";
5460
+ import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
4882
5461
  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";
5462
+ import { dirname as dirname3, join as join6 } from "path";
4884
5463
  var STATION_PROFILE_CACHE_FILENAME = "station-profile.md";
4885
5464
  var STATION_PROFILE_SOURCE_ID = "station-profile";
4886
5465
  var STATION_PROFILE_LAYER = "machine";
@@ -4894,19 +5473,19 @@ function homeDir(env = process.env) {
4894
5473
  return env["HOME"] || env["USERPROFILE"] || homedir2();
4895
5474
  }
4896
5475
  function getStationProfileCachePath(env = process.env) {
4897
- return join7(getRawStoreRoot(env), STATION_PROFILE_CACHE_FILENAME);
5476
+ return join6(getRawStoreRoot(env), STATION_PROFILE_CACHE_FILENAME);
4898
5477
  }
4899
5478
  function getMachinesManifestPath(env = process.env) {
4900
- return env[MACHINES_MANIFEST_PATH_ENV] || join7(homeDir(env), ".hasna", "machines", "machines.json");
5479
+ return env[MACHINES_MANIFEST_PATH_ENV] || join6(homeDir(env), ".hasna", "machines", "machines.json");
4901
5480
  }
4902
5481
  function getBunGlobalModulesDir(env = process.env) {
4903
- return join7(env[BUN_INSTALL_ENV] || join7(homeDir(env), ".bun"), "install", "global", "node_modules");
5482
+ return join6(env[BUN_INSTALL_ENV] || join6(homeDir(env), ".bun"), "install", "global", "node_modules");
4904
5483
  }
4905
5484
  function readMachinesManifest(path) {
4906
5485
  try {
4907
- if (!existsSync7(path))
5486
+ if (!existsSync6(path))
4908
5487
  return null;
4909
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
5488
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
4910
5489
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
4911
5490
  return null;
4912
5491
  const machines = parsed["machines"];
@@ -4962,7 +5541,7 @@ function resolveStationProfileMachine(env = process.env, options = {}) {
4962
5541
  const record = findLocalManifestMachine(readMachinesManifest(getMachinesManifestPath(env)), hostname);
4963
5542
  const home = homeDir(env);
4964
5543
  const platform = stringField(record, "platform") ?? osPlatform();
4965
- const workspacePath = stringField(record, "workspacePath") ?? join7(home, platform === "darwin" ? "Workspace" : "workspace");
5544
+ const workspacePath = stringField(record, "workspacePath") ?? join6(home, platform === "darwin" ? "Workspace" : "workspace");
4966
5545
  const machine = {
4967
5546
  id: stringField(record, "id") ?? hostname,
4968
5547
  hostname: stringField(record, "hostname") ?? hostname,
@@ -4979,9 +5558,9 @@ function resolveStationProfileMachine(env = process.env, options = {}) {
4979
5558
  return machine;
4980
5559
  }
4981
5560
  function scopedPackageNames(modulesDir, scope) {
4982
- const scopeDir = join7(modulesDir, scope);
5561
+ const scopeDir = join6(modulesDir, scope);
4983
5562
  try {
4984
- if (!existsSync7(scopeDir))
5563
+ if (!existsSync6(scopeDir))
4985
5564
  return null;
4986
5565
  return readdirNames(scopeDir).sort();
4987
5566
  } catch {
@@ -4991,7 +5570,7 @@ function scopedPackageNames(modulesDir, scope) {
4991
5570
  function readdirNames(dir) {
4992
5571
  return readdirSync2(dir).filter((name) => {
4993
5572
  try {
4994
- return lstatSync2(join7(dir, name)).isDirectory();
5573
+ return lstatSync2(join6(dir, name)).isDirectory();
4995
5574
  } catch {
4996
5575
  return false;
4997
5576
  }
@@ -5001,7 +5580,7 @@ function resolveStationProfilePackages(env = process.env) {
5001
5580
  const modulesDir = getBunGlobalModulesDir(env);
5002
5581
  let scopeDirs;
5003
5582
  try {
5004
- if (!existsSync7(modulesDir))
5583
+ if (!existsSync6(modulesDir))
5005
5584
  return null;
5006
5585
  scopeDirs = readdirNames(modulesDir).filter((name) => name.startsWith("@") && name.toLowerCase().includes("hasna"));
5007
5586
  } catch {
@@ -5076,9 +5655,9 @@ function refreshStationProfile(options = {}) {
5076
5655
  const path = getStationProfileCachePath(env);
5077
5656
  const generatedAt = new Date().toISOString();
5078
5657
  if (!options.dryRun) {
5079
- const existing = existsSync7(path) ? readFileSync3(path, "utf8") : null;
5658
+ const existing = existsSync6(path) ? readFileSync2(path, "utf8") : null;
5080
5659
  if (existing !== content) {
5081
- mkdirSync4(dirname2(path), { recursive: true });
5660
+ mkdirSync3(dirname3(path), { recursive: true });
5082
5661
  writeFileSync2(path, content, "utf8");
5083
5662
  }
5084
5663
  }
@@ -5095,9 +5674,9 @@ function refreshStationProfile(options = {}) {
5095
5674
  function readStationProfile(env = process.env) {
5096
5675
  const path = getStationProfileCachePath(env);
5097
5676
  try {
5098
- if (!existsSync7(path))
5677
+ if (!existsSync6(path))
5099
5678
  return null;
5100
- return readFileSync3(path, "utf8");
5679
+ return readFileSync2(path, "utf8");
5101
5680
  } catch {
5102
5681
  return null;
5103
5682
  }
@@ -5421,19 +6000,19 @@ async function ensureDangerousOperationGuardStandardConfig(store = resolveConfig
5421
6000
  }
5422
6001
 
5423
6002
  // src/lib/managed-skill-runtimes.ts
5424
- import { createHash as createHash2 } from "crypto";
6003
+ import { createHash as createHash4 } from "crypto";
5425
6004
  import { spawnSync as spawnSync2 } from "child_process";
5426
6005
  import {
5427
- existsSync as existsSync8,
6006
+ existsSync as existsSync7,
5428
6007
  lstatSync as lstatSync3,
5429
- mkdirSync as mkdirSync5,
5430
- readFileSync as readFileSync4,
6008
+ mkdirSync as mkdirSync4,
6009
+ readFileSync as readFileSync3,
5431
6010
  renameSync,
5432
- rmSync as rmSync3,
6011
+ rmSync as rmSync2,
5433
6012
  writeFileSync as writeFileSync3
5434
6013
  } from "fs";
5435
6014
  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";
6015
+ import { dirname as dirname4, join as join7, parse as parse2, relative as relative2, resolve as resolve5 } from "path";
5437
6016
  var INBOX_CONVERSATIONS_MINIMUM_VERSION = "0.5.28";
5438
6017
  var INBOX_SKILL_MARKERS = [
5439
6018
  [".claude", "skills", "inbox", "SKILL.md"],
@@ -5443,8 +6022,8 @@ var INBOX_SKILL_MARKERS = [
5443
6022
  [".cursor", "skills", "inbox", "SKILL.md"]
5444
6023
  ];
5445
6024
  var REQUIRED_WATCH_FLAGS = ["--from <agent>", "--all", "--full-content"];
5446
- function sha2562(content) {
5447
- return createHash2("sha256").update(content).digest("hex");
6025
+ function sha2564(content) {
6026
+ return createHash4("sha256").update(content).digest("hex");
5448
6027
  }
5449
6028
  function lstatOrNull(path) {
5450
6029
  try {
@@ -5459,8 +6038,8 @@ function findSymlinkedAncestor(path) {
5459
6038
  let current = parsed.root;
5460
6039
  const rel = relative2(parsed.root, normalized);
5461
6040
  for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
5462
- current = join8(current, segment);
5463
- if (!existsSync8(current))
6041
+ current = join7(current, segment);
6042
+ if (!existsSync7(current))
5464
6043
  return null;
5465
6044
  if (lstatSync3(current).isSymbolicLink())
5466
6045
  return current;
@@ -5476,16 +6055,7 @@ function assertNoSymlinkAncestors2(path) {
5476
6055
  function packagedInboxSkillPath(explicitPath) {
5477
6056
  if (explicitPath)
5478
6057
  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;
6058
+ throw new Error("Bundled skill contracts are retired; use the Skills CLI to manage private skills");
5489
6059
  }
5490
6060
  function readCanonicalSkill(explicitPath) {
5491
6061
  const assetPath = packagedInboxSkillPath(explicitPath);
@@ -5493,14 +6063,14 @@ function readCanonicalSkill(explicitPath) {
5493
6063
  if (!stat?.isFile()) {
5494
6064
  throw new Error("packaged inbox skill contract is not a regular file");
5495
6065
  }
5496
- const content = readFileSync4(assetPath, "utf8");
6066
+ const content = readFileSync3(assetPath, "utf8");
5497
6067
  if (!content.includes("conversations watch --from <agent> --all")) {
5498
6068
  throw new Error("packaged inbox skill contract does not declare the canonical conversations watcher");
5499
6069
  }
5500
6070
  if (!content.includes("There is no separate")) {
5501
6071
  throw new Error("packaged inbox skill contract does not retire the legacy executable");
5502
6072
  }
5503
- return { content, sha256: sha2562(content) };
6073
+ return { content, sha256: sha2564(content) };
5504
6074
  }
5505
6075
  function runProbe(command, args) {
5506
6076
  const result = spawnSync2(command, args, {
@@ -5531,7 +6101,7 @@ function compareVersions(left, right) {
5531
6101
  return 0;
5532
6102
  }
5533
6103
  function inspectSkillMarkers(homeDir2) {
5534
- return INBOX_SKILL_MARKERS.map((parts) => join8(homeDir2, ...parts)).map((path) => {
6104
+ return INBOX_SKILL_MARKERS.map((parts) => join7(homeDir2, ...parts)).map((path) => {
5535
6105
  const stat = lstatOrNull(path);
5536
6106
  if (!stat)
5537
6107
  return null;
@@ -5540,7 +6110,7 @@ function inspectSkillMarkers(homeDir2) {
5540
6110
  }
5541
6111
  return {
5542
6112
  path,
5543
- content: readFileSync4(path, "utf8"),
6113
+ content: readFileSync3(path, "utf8"),
5544
6114
  mode: stat.mode & 511,
5545
6115
  regular: true
5546
6116
  };
@@ -5575,7 +6145,7 @@ function inspectInbox(options) {
5575
6145
  let reason = "skill not installed";
5576
6146
  if (skillPresent) {
5577
6147
  const nonRegular = snapshots.some((snapshot) => !snapshot.regular);
5578
- const symlinkAncestor = snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname3(path))).find((found) => found !== null);
6148
+ const symlinkAncestor = snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname4(path))).find((found) => found !== null);
5579
6149
  if (nonRegular)
5580
6150
  reason = "managed skill target is not a regular file";
5581
6151
  else if (symlinkAncestor)
@@ -5659,13 +6229,13 @@ function projectUpdatedStatus(status, contractCount) {
5659
6229
  };
5660
6230
  }
5661
6231
  function cleanup(path) {
5662
- rmSync3(path, { force: true });
6232
+ rmSync2(path, { force: true });
5663
6233
  }
5664
6234
  function writeAtomic(path, content, mode) {
5665
- assertNoSymlinkAncestors2(dirname3(path));
6235
+ assertNoSymlinkAncestors2(dirname4(path));
5666
6236
  const tempPath = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
5667
6237
  try {
5668
- mkdirSync5(dirname3(path), { recursive: true, mode: 493 });
6238
+ mkdirSync4(dirname4(path), { recursive: true, mode: 493 });
5669
6239
  writeFileSync3(tempPath, content, { mode, flag: "wx" });
5670
6240
  renameSync(tempPath, path);
5671
6241
  } finally {
@@ -5674,7 +6244,7 @@ function writeAtomic(path, content, mode) {
5674
6244
  }
5675
6245
  var DEFAULT_SKILL_WRITE_FILE_OPERATIONS = {
5676
6246
  lstat: lstatOrNull,
5677
- read: (path) => readFileSync4(path, "utf8"),
6247
+ read: (path) => readFileSync3(path, "utf8"),
5678
6248
  write: writeAtomic
5679
6249
  };
5680
6250
  function writeSkillContractsTransactional(snapshots, canonicalContent, fileOperations = DEFAULT_SKILL_WRITE_FILE_OPERATIONS) {
@@ -5741,7 +6311,7 @@ async function reconcileManagedSkillRuntimes(options = {}) {
5741
6311
  dry_run: dryRun
5742
6312
  };
5743
6313
  }
5744
- const symlinkedAncestor = before.snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname3(path))).find((found) => found !== null);
6314
+ const symlinkedAncestor = before.snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname4(path))).find((found) => found !== null);
5745
6315
  if (symlinkedAncestor) {
5746
6316
  return {
5747
6317
  runtimes: [{ ...status, action: "failed", dry_run: dryRun, skill_contracts_changed: 0 }],
@@ -5818,28 +6388,28 @@ async function reconcileManagedSkillRuntimes(options = {}) {
5818
6388
  }
5819
6389
 
5820
6390
  // src/status.ts
5821
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
6391
+ import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
5822
6392
 
5823
6393
  // 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";
6394
+ import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
6395
+ import { dirname as dirname5, join as join8 } from "path";
5826
6396
  import { fileURLToPath } from "url";
5827
6397
  var cached = null;
5828
6398
  function getPackageVersion() {
5829
6399
  if (cached)
5830
6400
  return cached;
5831
6401
  try {
5832
- let dir = dirname4(fileURLToPath(import.meta.url));
6402
+ let dir = dirname5(fileURLToPath(import.meta.url));
5833
6403
  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"));
6404
+ const pkgPath = join8(dir, "package.json");
6405
+ if (existsSync8(pkgPath)) {
6406
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5837
6407
  if (pkg.name === "@hasna/instructions" && pkg.version) {
5838
6408
  cached = pkg.version;
5839
6409
  return cached;
5840
6410
  }
5841
6411
  }
5842
- const parent = dirname4(dir);
6412
+ const parent = dirname5(dir);
5843
6413
  if (parent === dir)
5844
6414
  break;
5845
6415
  dir = parent;
@@ -5908,11 +6478,11 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
5908
6478
  continue;
5909
6479
  knownTargets += 1;
5910
6480
  const targetPath = expandPath(config.target_path);
5911
- if (!existsSync10(targetPath)) {
6481
+ if (!existsSync9(targetPath)) {
5912
6482
  missingTargets += 1;
5913
6483
  continue;
5914
6484
  }
5915
- const disk = readFileSync6(targetPath, "utf-8");
6485
+ const disk = readFileSync5(targetPath, "utf-8");
5916
6486
  const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(config.target_path, config.format));
5917
6487
  if (redactedDisk !== config.content) {
5918
6488
  driftedTargets += 1;
@@ -6004,9 +6574,9 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
6004
6574
  }
6005
6575
 
6006
6576
  // 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";
6577
+ import { createHash as createHash5 } from "crypto";
6578
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
6579
+ import { join as join9 } from "path";
6010
6580
  var PROVIDER_CONTEXT_DIR = ".hasna/provider-context";
6011
6581
  var PROVIDER_CONTEXT_MANIFEST = "manifest.json";
6012
6582
  var PROVIDER_CONTEXT_SCHEMA = "hasna.instructions.provider-context/v1";
@@ -6141,8 +6711,8 @@ function renderPerEndpointFragment(entry) {
6141
6711
  function renderProviderFragment(entry) {
6142
6712
  return entry ? renderPerEndpointFragment(entry) : INVARIANT_FRAGMENT;
6143
6713
  }
6144
- function sha2563(content) {
6145
- return createHash3("sha256").update(content).digest("hex");
6714
+ function sha2565(content) {
6715
+ return createHash5("sha256").update(content).digest("hex");
6146
6716
  }
6147
6717
  function resolveAndRenderProviderContext(opts) {
6148
6718
  const entry = matchProviderEndpoint(opts.origin);
@@ -6151,18 +6721,18 @@ function resolveAndRenderProviderContext(opts) {
6151
6721
  const recordedEndpoint = originAccepted ? `${opts.origin.host}${opts.origin.pathPrefix || ""}` : null;
6152
6722
  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
6723
  const content = renderProviderFragment(entry);
6154
- const dir = join10(opts.homeDir, PROVIDER_CONTEXT_DIR);
6155
- if (!existsSync11(dir))
6156
- mkdirSync6(dir, { recursive: true });
6724
+ const dir = join9(opts.homeDir, PROVIDER_CONTEXT_DIR);
6725
+ if (!existsSync10(dir))
6726
+ mkdirSync5(dir, { recursive: true });
6157
6727
  const filename = `${entry ? entry.key : "invariant"}.md`;
6158
- const fragmentPath = join10(dir, filename);
6159
- const fragmentSha256 = sha2563(content);
6728
+ const fragmentPath = join9(dir, filename);
6729
+ const fragmentSha256 = sha2565(content);
6160
6730
  writeFileSync4(fragmentPath, content, "utf8");
6161
- const manifestPath = join10(dir, PROVIDER_CONTEXT_MANIFEST);
6731
+ const manifestPath = join9(dir, PROVIDER_CONTEXT_MANIFEST);
6162
6732
  let manifest = { schema: PROVIDER_CONTEXT_SCHEMA, fragments: {} };
6163
6733
  try {
6164
- if (existsSync11(manifestPath)) {
6165
- const parsed = JSON.parse(readFileSync7(manifestPath, "utf8"));
6734
+ if (existsSync10(manifestPath)) {
6735
+ const parsed = JSON.parse(readFileSync6(manifestPath, "utf8"));
6166
6736
  if (parsed && typeof parsed === "object")
6167
6737
  manifest = parsed;
6168
6738
  }
@@ -6305,7 +6875,7 @@ function parseSessionSource(value, order) {
6305
6875
  if (!path)
6306
6876
  throw new Error(`Invalid --source "${value}" (expected path or id=path)`);
6307
6877
  const absPath = resolveSessionPath(path);
6308
- if (!existsSync12(absPath))
6878
+ if (!existsSync11(absPath))
6309
6879
  throw new Error(`Instruction source file not found: ${absPath}`);
6310
6880
  const content = readSessionInstructionSourceFile(absPath);
6311
6881
  const source = sourceFromFilePath(absPath, content, order);
@@ -6359,7 +6929,7 @@ function readSessionInstructionSourceFile(path) {
6359
6929
  if (stat.size > SESSION_MANAGED_INPUT_MAX_BYTES) {
6360
6930
  throw new Error(`SESSION_SOURCE_INPUT_TOO_LARGE: instruction source file exceeds ${SESSION_MANAGED_INPUT_MAX_BYTES} bytes`);
6361
6931
  }
6362
- return readFileSync8(path, "utf-8");
6932
+ return readFileSync7(path, "utf-8");
6363
6933
  }
6364
6934
  function parseLayeredReference(value) {
6365
6935
  const trimmed = value.trim();
@@ -6386,9 +6956,9 @@ async function collectSessionSources(opts, tool, store) {
6386
6956
  }
6387
6957
  for (const value of opts.identityExport ?? []) {
6388
6958
  const path = resolveSessionPath(value);
6389
- if (!existsSync12(path))
6959
+ if (!existsSync11(path))
6390
6960
  throw new Error(`Identity instruction export not found: ${path}`);
6391
- const parsed = JSON.parse(readFileSync8(path, "utf-8"));
6961
+ const parsed = JSON.parse(readFileSync7(path, "utf-8"));
6392
6962
  sources.push(...sourcesFromIdentityExport(parsed, { path, tool, orderOffset: sources.length }));
6393
6963
  }
6394
6964
  return sources.map((source) => {
@@ -6520,7 +7090,7 @@ function readProjectContextBundleOption(value, allowMissing = false) {
6520
7090
  if (value === "-")
6521
7091
  return { json: readBoundedProjectContextStdin() };
6522
7092
  const path = resolveSessionPath(value);
6523
- if (!existsSync12(path)) {
7093
+ if (!existsSync11(path)) {
6524
7094
  if (allowMissing)
6525
7095
  return {};
6526
7096
  throw new ProjectContextError("PROJECT_CONTEXT_INPUT_MISSING", `bundle file not found: ${path}`);
@@ -6532,7 +7102,7 @@ function readProjectContextBundleOption(value, allowMissing = false) {
6532
7102
  if (stat.size > PROJECT_CONTEXT_MAX_INPUT_BYTES) {
6533
7103
  throw new ProjectContextError("PROJECT_CONTEXT_INPUT_TOO_LARGE", `bundle exceeds ${PROJECT_CONTEXT_MAX_INPUT_BYTES} bytes`);
6534
7104
  }
6535
- return { json: readFileSync8(path, "utf8"), sourcePath: path };
7105
+ return { json: readFileSync7(path, "utf8"), sourcePath: path };
6536
7106
  }
6537
7107
  function readBoundedProjectContextStdin() {
6538
7108
  const chunks = [];
@@ -6692,15 +7262,15 @@ program.command("tag <id>").description("Add or remove tags on a stored config (
6692
7262
  });
6693
7263
  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
7264
  const abs = resolve6(filePath);
6695
- if (!existsSync12(abs)) {
7265
+ if (!existsSync11(abs)) {
6696
7266
  console.error(chalk.red(`File not found: ${abs}`));
6697
7267
  process.exit(1);
6698
7268
  }
6699
- const rawContent = readFileSync8(abs, "utf-8");
7269
+ const rawContent = readFileSync7(abs, "utf-8");
6700
7270
  const storedFmt = detectFormat(abs);
6701
7271
  const fmt = redactFormatForTarget(abs, storedFmt);
6702
7272
  const { content, redacted, isTemplate } = redactContent(rawContent, fmt);
6703
- const targetPath = abs.startsWith(homedir4()) ? abs.replace(homedir4(), "~") : abs;
7273
+ const targetPath = compactPathForConfigHome(abs);
6704
7274
  const name = opts.name || filePath.split("/").pop();
6705
7275
  const store = resolveConfigStore();
6706
7276
  const allConfigs = await store.listConfigs();
@@ -6875,7 +7445,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
6875
7445
  for (const entry of entries) {
6876
7446
  if (!entry.isDirectory())
6877
7447
  continue;
6878
- const projDir = join11(absDir, entry.name);
7448
+ const projDir = join10(absDir, entry.name);
6879
7449
  const hasAgentConfig = [
6880
7450
  "CLAUDE.md",
6881
7451
  ".mcp.json",
@@ -6888,7 +7458,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
6888
7458
  ".aicopilot",
6889
7459
  ".cursor",
6890
7460
  ".agents"
6891
- ].some((marker) => existsSync12(join11(projDir, marker)));
7461
+ ].some((marker) => existsSync11(join10(projDir, marker)));
6892
7462
  if (!hasAgentConfig)
6893
7463
  continue;
6894
7464
  const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun, store });
@@ -6918,28 +7488,25 @@ program.command("sync").description("Sync known AI coding configs from disk into
6918
7488
  }
6919
7489
  }
6920
7490
  });
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) => {
7491
+ 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
7492
  const result = await exportConfigs(opts.output, {
6923
7493
  filter: opts.category ? { category: opts.category } : undefined,
6924
7494
  store: resolveConfigStore()
6925
7495
  });
6926
- console.log(chalk.green("\u2713") + ` Exported ${result.count} configs to ${result.path}`);
7496
+ 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
7497
  });
6928
- program.command("import <file>").description("Import configs from a tar.gz bundle").option("--overwrite", "overwrite existing configs").action(async (file, opts) => {
7498
+ 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
7499
  const result = await importConfigs(file, {
6930
7500
  conflict: opts.overwrite ? "overwrite" : "skip",
6931
7501
  store: resolveConfigStore()
6932
7502
  });
7503
+ if (result.errors.length > 0)
7504
+ throw new Error(`Import failed: ${result.errors.join("; ")}`);
6933
7505
  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
7506
  });
6940
7507
  program.command("whoami").description("Show setup summary").action(async () => {
6941
7508
  const store = resolveConfigStore();
6942
- const dbPath = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join11(getRawStoreRoot(), "instructions.db");
7509
+ const dbPath = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join10(getRawStoreRoot(), "instructions.db");
6943
7510
  const stats = await store.getConfigStats();
6944
7511
  console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
6945
7512
  console.log(chalk.cyan(store.mode === "api" ? "API:" : "DB:") + " " + dbPath);
@@ -7647,7 +8214,7 @@ templateCmd.command("vars <id>").description("Show template variables").action(a
7647
8214
  });
7648
8215
  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
8216
  try {
7650
- const { renderTemplate } = await import("../chunks/template-ke972qqe.js");
8217
+ const { renderTemplate } = await import("../chunks/template-wgnzdn7h.js");
7651
8218
  const c = await resolveConfigStore().getConfig(id);
7652
8219
  const vars = {};
7653
8220
  if (opts.var) {
@@ -7661,7 +8228,7 @@ templateCmd.command("render <id>").description("Render a template config with va
7661
8228
  }
7662
8229
  }
7663
8230
  if (opts.env) {
7664
- const { extractTemplateVars: extractTemplateVars2 } = await import("../chunks/template-ke972qqe.js");
8231
+ const { extractTemplateVars: extractTemplateVars2 } = await import("../chunks/template-wgnzdn7h.js");
7665
8232
  for (const v of extractTemplateVars2(c.content)) {
7666
8233
  if (!(v.name in vars) && process.env[v.name]) {
7667
8234
  vars[v.name] = process.env[v.name];
@@ -7710,7 +8277,7 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
7710
8277
  } else if (opts.all) {
7711
8278
  configs = await store.listConfigs(opts.category ? { kind: "file", category: opts.category } : { kind: "file" });
7712
8279
  } else {
7713
- const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await import("../chunks/sync-m46fc257.js");
8280
+ const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await import("../chunks/sync-2075deve.js");
7714
8281
  const slugs = [
7715
8282
  ...KNOWN_CONFIGS2.filter((k) => !k.rulesDir).map((k) => k.name)
7716
8283
  ];
@@ -8004,7 +8571,7 @@ command = "${mcpBinary}"
8004
8571
  args = []
8005
8572
  `;
8006
8573
  if (ex(configPath)) {
8007
- const content = readFileSync8(configPath, "utf-8");
8574
+ const content = readFileSync7(configPath, "utf-8");
8008
8575
  if (findTomlTableLines(content.split(`
8009
8576
  `), "mcp_servers.configs")) {
8010
8577
  console.log(chalk.dim("= Already installed in Codex"));
@@ -8066,7 +8633,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
8066
8633
  console.log(chalk.dim("= Not installed in Codex (no config.toml)"));
8067
8634
  continue;
8068
8635
  }
8069
- const content = readFileSync8(configPath, "utf-8");
8636
+ const content = readFileSync7(configPath, "utf-8");
8070
8637
  const lines = content.split(`
8071
8638
  `);
8072
8639
  const table = findTomlTableLines(lines, "mcp_servers.configs");
@@ -8085,7 +8652,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
8085
8652
  console.log(chalk.green("\u2713") + " Removed from Codex");
8086
8653
  } else if (target === "antigravity") {
8087
8654
  const { existsSync: ex, readFileSync: rf, writeFileSync: wf } = await import("fs");
8088
- const configPath = join11(homedir4(), ".gemini", "config", "mcp_config.json");
8655
+ const configPath = join10(homedir4(), ".gemini", "config", "mcp_config.json");
8089
8656
  if (!ex(configPath)) {
8090
8657
  console.log(chalk.dim("= Not installed in Antigravity (no mcp_config.json)"));
8091
8658
  continue;
@@ -8113,6 +8680,40 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
8113
8680
  if (failed)
8114
8681
  process.exitCode = 1;
8115
8682
  });
8683
+ 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) => {
8684
+ if (!selectsInstructionsLocalStore(process.env)) {
8685
+ throw new Error("migrate-legacy is local-only: set HASNA_INSTRUCTIONS_LOCAL=1 and remove every hosted Instructions authority/credential first");
8686
+ }
8687
+ if (!opts.confirmLocal) {
8688
+ throw new Error("migrate-legacy requires --confirm-local");
8689
+ }
8690
+ const destinationPath = resolve6(opts.destination || process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join10(getRawStoreRoot(), "instructions.db"));
8691
+ const database = await import("../chunks/database-wstmr56z.js");
8692
+ database.getDatabase(destinationPath);
8693
+ database.resetDatabase();
8694
+ const { migrateLegacyInstructionsStore } = await import("../chunks/legacy-store-migration-z2m5yzyk.js");
8695
+ const result = migrateLegacyInstructionsStore({
8696
+ ...opts.source ? { sourcePath: resolve6(opts.source) } : {},
8697
+ destinationPath,
8698
+ ...opts.backup ? { backupPath: resolve6(opts.backup) } : {},
8699
+ dryRun: !opts.apply,
8700
+ ...opts.mergePreserveDestination ? { mergePolicy: "preserve-destination" } : {},
8701
+ operatorConfirmed: true
8702
+ });
8703
+ if (opts.json) {
8704
+ printJson(result);
8705
+ return;
8706
+ }
8707
+ const total = Object.values(result.migrated).reduce((sum, count) => sum + count, 0);
8708
+ const skipped = Object.values(result.skipped).reduce((sum, count) => sum + count, 0);
8709
+ const mode = result.dryRun ? "Dry-run" : "Migrated";
8710
+ console.log(chalk.green("\u2713") + ` ${mode}: ${total} row(s), skipped ${skipped}, conflicts ${result.conflicts}`);
8711
+ if (result.snapshotsCreated > 0) {
8712
+ console.log(chalk.dim(` created ${result.snapshotsCreated} missing current-version snapshot(s)`));
8713
+ }
8714
+ if (result.backupCreated)
8715
+ console.log(chalk.dim(" destination backup created before mutation"));
8716
+ });
8116
8717
  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
8718
  const store = resolveConfigStore();
8118
8719
  if (opts.force) {
@@ -8172,7 +8773,7 @@ DB stats:`));
8172
8773
  if (count > 0)
8173
8774
  console.log(` ${key.padEnd(18)} ${count}`);
8174
8775
  }
8175
- const location = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join11(getRawStoreRoot(), "instructions.db");
8776
+ const location = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join10(getRawStoreRoot(), "instructions.db");
8176
8777
  console.log(chalk.dim(`
8177
8778
  ${store.mode === "api" ? "API" : "DB"}: ${location}`));
8178
8779
  });
@@ -8231,24 +8832,187 @@ managedSkillsCmd.command("apply").option("--dry-run", "preview without writing")
8231
8832
  if (report.failed > 0)
8232
8833
  process.exitCode = 1;
8233
8834
  });
8835
+ function normalizeBackupVersionOptions(opts) {
8836
+ const payloadVersionId = opts.payloadVersionId?.trim();
8837
+ const manifestVersionId = opts.manifestVersionId?.trim();
8838
+ if (Boolean(payloadVersionId) !== Boolean(manifestVersionId)) {
8839
+ throw new Error("S3 backup payload and manifest version ids must be provided together");
8840
+ }
8841
+ return payloadVersionId && manifestVersionId ? { payloadVersionId, manifestVersionId } : {};
8842
+ }
8843
+ var storageCmd = program.command("storage").description("Inspect and use the optional native S3 backup plane");
8844
+ storageCmd.command("status").description("Show redacted S3 configuration status without opening SQLite or contacting S3").option("--json", "output JSON").action(async (opts) => {
8845
+ const { loadInstructionsS3Config } = await import("../chunks/s3-config-hhght91k.js");
8846
+ const config = loadInstructionsS3Config(process.env);
8847
+ const status = config ? {
8848
+ configured: true,
8849
+ provider: "s3",
8850
+ region: config.region,
8851
+ prefix: config.prefix,
8852
+ endpointConfigured: Boolean(config.endpoint),
8853
+ forcePathStyle: config.forcePathStyle,
8854
+ credentialSource: config.credentials ? "static-explicit" : "runtime-default-chain",
8855
+ includesCredentialValues: false,
8856
+ databaseAuthority: false
8857
+ } : {
8858
+ configured: false,
8859
+ provider: null,
8860
+ region: null,
8861
+ prefix: null,
8862
+ endpointConfigured: false,
8863
+ forcePathStyle: false,
8864
+ credentialSource: null,
8865
+ includesCredentialValues: false,
8866
+ databaseAuthority: false
8867
+ };
8868
+ if (opts.json)
8869
+ printJson(status);
8870
+ else if (!config)
8871
+ console.log(chalk.dim("S3 backup storage is not configured."));
8872
+ else
8873
+ console.log(chalk.green("\u2713") + ` S3 backup storage configured (${config.region}, prefix ${config.prefix}; database authority: no)`);
8874
+ });
8875
+ var storageBackupCmd = storageCmd.command("backup").description("Push, pull, and verify immutable Instructions backup objects");
8876
+ 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) => {
8877
+ const abs = resolve6(file);
8878
+ if (!existsSync11(abs) || !lstatSync4(abs).isFile())
8879
+ throw new Error(`Backup file not found: ${abs}`);
8880
+ const [{ loadInstructionsS3Config }, backup, objectStore] = await Promise.all([
8881
+ import("../chunks/s3-config-hhght91k.js"),
8882
+ import("../chunks/s3-backup-n2z4fwyh.js"),
8883
+ import("../chunks/s3-object-store-90tgbh6k.js")
8884
+ ]);
8885
+ const config = loadInstructionsS3Config(process.env);
8886
+ if (!config)
8887
+ throw new Error("S3 backup storage is not configured (set HASNA_INSTRUCTIONS_S3_BUCKET)");
8888
+ const bytes = await Bun.file(abs).bytes();
8889
+ if (opts.dryRun) {
8890
+ const plan = await backup.planInstructionsBackupPush({
8891
+ store: { get: async () => {
8892
+ return;
8893
+ }, head: async () => {
8894
+ return;
8895
+ } },
8896
+ prefix: config.prefix,
8897
+ backupId: opts.id,
8898
+ bytes,
8899
+ contentType: opts.contentType
8900
+ });
8901
+ if (opts.json)
8902
+ printJson(plan);
8903
+ else
8904
+ console.log(chalk.dim(`Would upload ${plan.sizeBytes} bytes to ${plan.payloadKey} (sha256 ${plan.sha256})`));
8905
+ return;
8906
+ }
8907
+ const result = await backup.pushInstructionsBackup({
8908
+ store: objectStore.createInstructionsS3ObjectStore(config),
8909
+ prefix: config.prefix,
8910
+ backupId: opts.id,
8911
+ bytes,
8912
+ contentType: opts.contentType
8913
+ });
8914
+ const safe = {
8915
+ status: result.status,
8916
+ backupId: result.manifest.backupId,
8917
+ sha256: result.manifest.sha256,
8918
+ sizeBytes: result.manifest.sizeBytes,
8919
+ ...result.versions ? result.versions : {}
8920
+ };
8921
+ if (opts.json)
8922
+ printJson(safe);
8923
+ else
8924
+ console.log(chalk.green("\u2713") + ` S3 backup ${result.status}: ${safe.backupId} (${safe.sizeBytes} bytes, sha256 ${safe.sha256})`);
8925
+ });
8926
+ 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) => {
8927
+ const versions = normalizeBackupVersionOptions(opts);
8928
+ const [{ loadInstructionsS3Config }, backup, objectStore] = await Promise.all([
8929
+ import("../chunks/s3-config-hhght91k.js"),
8930
+ import("../chunks/s3-backup-n2z4fwyh.js"),
8931
+ import("../chunks/s3-object-store-90tgbh6k.js")
8932
+ ]);
8933
+ const config = loadInstructionsS3Config(process.env);
8934
+ if (!config)
8935
+ throw new Error("S3 backup storage is not configured (set HASNA_INSTRUCTIONS_S3_BUCKET)");
8936
+ const verified = await backup.verifyInstructionsBackup({
8937
+ store: objectStore.createInstructionsS3ObjectStore(config),
8938
+ prefix: config.prefix,
8939
+ backupId,
8940
+ ...versions
8941
+ });
8942
+ const safe = {
8943
+ ...verified,
8944
+ ...verified.versions ? verified.versions : {},
8945
+ versions: undefined
8946
+ };
8947
+ if (opts.json)
8948
+ printJson(safe);
8949
+ else
8950
+ console.log(chalk.green("\u2713") + ` Verified ${verified.backupId}: ${verified.sizeBytes} bytes, sha256 ${verified.sha256}${verified.versionPinned ? " (exact S3 versions)" : ""}`);
8951
+ });
8952
+ 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) => {
8953
+ const versions = normalizeBackupVersionOptions(opts);
8954
+ const [{ loadInstructionsS3Config }, backup, objectStore] = await Promise.all([
8955
+ import("../chunks/s3-config-hhght91k.js"),
8956
+ import("../chunks/s3-backup-n2z4fwyh.js"),
8957
+ import("../chunks/s3-object-store-90tgbh6k.js")
8958
+ ]);
8959
+ const config = loadInstructionsS3Config(process.env);
8960
+ if (!config)
8961
+ throw new Error("S3 backup storage is not configured (set HASNA_INSTRUCTIONS_S3_BUCKET)");
8962
+ const pulled = await backup.pullInstructionsBackup({
8963
+ store: objectStore.createInstructionsS3ObjectStore(config),
8964
+ prefix: config.prefix,
8965
+ backupId,
8966
+ ...versions
8967
+ });
8968
+ const output = resolve6(opts.output);
8969
+ if (existsSync11(output)) {
8970
+ if (!opts.force)
8971
+ throw new Error(`Refusing to overwrite existing backup file: ${output}`);
8972
+ if (!lstatSync4(output).isFile() || lstatSync4(output).isSymbolicLink()) {
8973
+ throw new Error(`Refusing to replace a non-regular backup destination: ${output}`);
8974
+ }
8975
+ }
8976
+ mkdirSync6(dirname6(output), { recursive: true, mode: 448 });
8977
+ const temp = join10(dirname6(output), `.${basename(output)}.tmp-${process.pid}-${Date.now()}`);
8978
+ try {
8979
+ writeFileSync5(temp, pulled.bytes, { flag: "wx", mode: 384 });
8980
+ renameSync2(temp, output);
8981
+ } catch (error) {
8982
+ try {
8983
+ unlinkSync(temp);
8984
+ } catch {}
8985
+ throw error;
8986
+ }
8987
+ const safe = {
8988
+ backupId: pulled.manifest.backupId,
8989
+ output,
8990
+ sha256: pulled.manifest.sha256,
8991
+ sizeBytes: pulled.manifest.sizeBytes,
8992
+ versionPinned: pulled.versionPinned,
8993
+ ...pulled.versions ? pulled.versions : {}
8994
+ };
8995
+ if (opts.json)
8996
+ printJson(safe);
8997
+ else
8998
+ console.log(chalk.green("\u2713") + ` Restored ${safe.backupId} to ${safe.output} (${safe.sizeBytes} bytes)`);
8999
+ });
8234
9000
  program.command("backup").description("Export configs to a timestamped backup file").action(async () => {
8235
9001
  const { mkdirSync: mk } = await import("fs");
8236
- const backupDir = join11(getRawStoreRoot(), "backups");
9002
+ const backupDir = join10(getRawStoreRoot(), "backups");
8237
9003
  mk(backupDir, { recursive: true });
8238
9004
  const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 19);
8239
- const outPath = join11(backupDir, `configs-${ts}.tar.gz`);
9005
+ const outPath = join10(backupDir, `configs-${ts}.tar.gz`);
8240
9006
  const result = await exportConfigs(outPath, { store: resolveConfigStore() });
8241
9007
  const { statSync: st } = await import("fs");
8242
9008
  const size = st(outPath).size;
8243
9009
  console.log(chalk.green("\u2713") + ` Backup: ${result.count} configs \u2192 ${outPath} (${(size / 1024).toFixed(1)}KB)`);
8244
9010
  });
8245
- program.command("restore <file>").description("Restore configs from a backup file").option("--overwrite", "overwrite existing configs (default: skip)").action(async (file, opts) => {
9011
+ 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
9012
  const result = await importConfigs(file, { conflict: opts.overwrite ? "overwrite" : "skip", store: resolveConfigStore() });
9013
+ if (result.errors.length > 0)
9014
+ throw new Error(`Restore failed: ${result.errors.join("; ")}`);
8247
9015
  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
9016
  });
8253
9017
  program.command("doctor").description("Validate configs: syntax, permissions, missing files, secrets").action(async () => {
8254
9018
  const store = resolveConfigStore();
@@ -8264,9 +9028,9 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
8264
9028
  console.log(chalk.cyan("Known files on disk:"));
8265
9029
  for (const k of KNOWN_CONFIGS) {
8266
9030
  if (k.rulesDir) {
8267
- existsSync12(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail2(`${k.rulesDir}/ not found`);
9031
+ existsSync11(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail2(`${k.rulesDir}/ not found`);
8268
9032
  } else {
8269
- existsSync12(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail2(`${k.path} not found`);
9033
+ existsSync11(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail2(`${k.path} not found`);
8270
9034
  }
8271
9035
  }
8272
9036
  const allConfigs = await store.listConfigs();
@@ -8412,7 +9176,7 @@ program.command("watch").description("Watch known config files for changes and a
8412
9176
  const store = resolveConfigStore();
8413
9177
  const interval = Number(opts.interval);
8414
9178
  const { statSync: st } = await import("fs");
8415
- const { expandPath: expandPath2 } = await import("../chunks/apply-sfzja1f3.js");
9179
+ const { expandPath: expandPath2 } = await import("../chunks/apply-jp13sa1e.js");
8416
9180
  console.log(chalk.bold("@hasna/instructions watch") + chalk.dim(` \u2014 polling every ${interval}ms`));
8417
9181
  console.log(chalk.dim(`Watching known config files for changes\u2026
8418
9182
  `));
@@ -8420,16 +9184,16 @@ program.command("watch").description("Watch known config files for changes and a
8420
9184
  for (const k of KNOWN_CONFIGS) {
8421
9185
  if (k.rulesDir) {
8422
9186
  const absDir = expandPath2(k.rulesDir);
8423
- if (!existsSync12(absDir))
9187
+ if (!existsSync11(absDir))
8424
9188
  continue;
8425
9189
  const { readdirSync: readdirSync3 } = await import("fs");
8426
9190
  for (const f of readdirSync3(absDir).filter((f2) => f2.endsWith(".md"))) {
8427
- const abs = join11(absDir, f);
9191
+ const abs = join10(absDir, f);
8428
9192
  mtimes.set(abs, st(abs).mtimeMs);
8429
9193
  }
8430
9194
  } else {
8431
9195
  const abs = expandPath2(k.path);
8432
- if (existsSync12(abs))
9196
+ if (existsSync11(abs))
8433
9197
  mtimes.set(abs, st(abs).mtimeMs);
8434
9198
  }
8435
9199
  }
@@ -8437,7 +9201,7 @@ program.command("watch").description("Watch known config files for changes and a
8437
9201
  const tick = async () => {
8438
9202
  let changed = 0;
8439
9203
  for (const [abs, oldMtime] of mtimes) {
8440
- if (!existsSync12(abs))
9204
+ if (!existsSync11(abs))
8441
9205
  continue;
8442
9206
  const newMtime = st(abs).mtimeMs;
8443
9207
  if (newMtime !== oldMtime) {
@@ -8449,10 +9213,10 @@ program.command("watch").description("Watch known config files for changes and a
8449
9213
  for (const k of KNOWN_CONFIGS) {
8450
9214
  if (k.rulesDir) {
8451
9215
  const absDir = expandPath2(k.rulesDir);
8452
- if (!existsSync12(absDir))
9216
+ if (!existsSync11(absDir))
8453
9217
  continue;
8454
9218
  for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
8455
- const abs = join11(absDir, f);
9219
+ const abs = join10(absDir, f);
8456
9220
  if (!mtimes.has(abs)) {
8457
9221
  mtimes.set(abs, st(abs).mtimeMs);
8458
9222
  changed++;
@@ -8460,7 +9224,7 @@ program.command("watch").description("Watch known config files for changes and a
8460
9224
  }
8461
9225
  } else {
8462
9226
  const abs = expandPath2(k.path);
8463
- if (existsSync12(abs) && !mtimes.has(abs)) {
9227
+ if (existsSync11(abs) && !mtimes.has(abs)) {
8464
9228
  mtimes.set(abs, st(abs).mtimeMs);
8465
9229
  changed++;
8466
9230
  }
@@ -8488,11 +9252,11 @@ program.command("report").description("Summary of stored configs, drift, and eco
8488
9252
  if (!c.target_path)
8489
9253
  continue;
8490
9254
  const abs = expandPath(c.target_path);
8491
- if (!existsSync12(abs)) {
9255
+ if (!existsSync11(abs)) {
8492
9256
  missing++;
8493
9257
  continue;
8494
9258
  }
8495
- const disk = readFileSync8(abs, "utf-8");
9259
+ const disk = readFileSync7(abs, "utf-8");
8496
9260
  const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(c.target_path, c.format));
8497
9261
  if (redactedDisk !== c.content)
8498
9262
  drifted++;
@@ -8559,7 +9323,7 @@ program.command("clean").description("Remove configs from DB whose target files
8559
9323
  if (!c.target_path)
8560
9324
  continue;
8561
9325
  const abs = expandPath(c.target_path);
8562
- if (!existsSync12(abs)) {
9326
+ if (!existsSync11(abs)) {
8563
9327
  if (printed < maxPrinted) {
8564
9328
  if (opts.dryRun) {
8565
9329
  console.log(chalk.yellow(" would remove:") + ` ${c.slug} ${chalk.dim(`(${truncateMiddle(c.target_path, 88)})`)}`);