@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.
- package/README.md +123 -6
- package/dist/chunks/{apply-ynfn1av7.js → apply-7svah2qm.js} +5 -3
- package/dist/chunks/{apply-sfzja1f3.js → apply-jp13sa1e.js} +6 -3
- package/dist/chunks/database-wstmr56z.js +23 -0
- package/dist/chunks/{index-85n5wnd8.js → index-1cnyjnd7.js} +5 -7
- package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
- package/dist/chunks/{index-v7dkb384.js → index-4pfq42t5.js} +129 -77
- package/dist/chunks/index-5d7zdftc.js +186 -0
- package/dist/chunks/{index-1hpwskwd.js → index-7m4j8a33.js} +205 -329
- package/dist/chunks/index-8rb7ng2y.js +148 -0
- package/dist/chunks/index-arrrx0ed.js +344 -0
- package/dist/chunks/{index-9p9gn7gh.js → index-bzyxvwcd.js} +5 -7
- package/dist/chunks/index-fpeyzn3n.js +186 -0
- package/dist/chunks/{index-axhwg61p.js → index-fwbs6tma.js} +15 -4
- package/dist/chunks/index-n3fjspr0.js +119 -0
- package/dist/chunks/{index-e9epbsvs.js → index-z3ma4aqk.js} +15 -4
- package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
- package/dist/chunks/{local-a9nen65z.js → local-5wvv5xv3.js} +2 -2
- package/dist/chunks/{local-0d8qrc92.js → local-zxa5dpf4.js} +13 -188
- package/dist/chunks/s3-backup-n2z4fwyh.js +240 -0
- package/dist/chunks/s3-config-hhght91k.js +14 -0
- package/dist/chunks/s3-object-store-90tgbh6k.js +14 -0
- package/dist/chunks/{sync-m46fc257.js → sync-2075deve.js} +5 -4
- package/dist/chunks/{sync-vy1yjvs6.js → sync-r8t7zsp3.js} +4 -4
- package/dist/chunks/{template-02wjvv05.js → template-fsfxpe4p.js} +1 -1
- package/dist/chunks/{template-ke972qqe.js → template-wgnzdn7h.js} +1 -1
- package/dist/cli/index.js +1011 -247
- package/dist/data/config-store.d.ts +1 -0
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/index.js +840 -215
- package/dist/lib/apply.d.ts +7 -0
- package/dist/lib/apply.d.ts.map +1 -1
- package/dist/lib/compact-output.d.ts +1 -0
- package/dist/lib/compact-output.d.ts.map +1 -1
- package/dist/lib/export.d.ts +22 -3
- package/dist/lib/export.d.ts.map +1 -1
- package/dist/lib/import.d.ts +18 -0
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/legacy-store-migration.d.ts +39 -0
- package/dist/lib/legacy-store-migration.d.ts.map +1 -0
- package/dist/lib/managed-skill-runtimes.d.ts +1 -0
- package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
- package/dist/lib/sync-dir.d.ts.map +1 -1
- package/dist/mcp/index.js +31 -20
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +205 -10
- package/dist/sdk/resolve.d.ts +1 -1
- package/dist/sdk/resolve.d.ts.map +1 -1
- package/dist/sdk/v1-client.d.ts +57 -0
- package/dist/sdk/v1-client.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +212 -21
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts +13 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1180 -432
- package/dist/server/openapi.d.ts +1264 -92
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts +25 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/cloud-store.d.ts +57 -1
- package/dist/storage/cloud-store.d.ts.map +1 -1
- package/dist/storage/index.d.ts +7 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +725 -0
- package/dist/storage/s3-backup.d.ts +76 -0
- package/dist/storage/s3-backup.d.ts.map +1 -0
- package/dist/storage/s3-config.d.ts +46 -0
- package/dist/storage/s3-config.d.ts.map +1 -0
- package/dist/storage/s3-object-store.d.ts +70 -0
- package/dist/storage/s3-object-store.d.ts.map +1 -0
- package/dist/types/index.d.ts +65 -3
- package/dist/types/index.d.ts.map +1 -1
- package/hasna.contract.json +110 -0
- package/migrations/0001_instructions.sql +63 -0
- package/migrations/0002_api_keys.sql +20 -0
- package/migrations/0003_idempotency_receipts.sql +24 -0
- package/package.json +22 -11
- 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-
|
|
12
|
+
} from "../chunks/index-1cnyjnd7.js";
|
|
16
13
|
import {
|
|
17
14
|
applyConfigsWithReport,
|
|
15
|
+
compactPathForConfigHome,
|
|
18
16
|
expandPath,
|
|
19
17
|
normalizeTargetPath
|
|
20
|
-
} from "../chunks/index-
|
|
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-
|
|
64
|
+
} from "../chunks/index-7m4j8a33.js";
|
|
68
65
|
import {
|
|
66
|
+
INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
|
|
69
67
|
extractTemplateVars
|
|
70
|
-
} from "../chunks/index-
|
|
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
|
|
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
|
|
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 {
|
|
3824
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3831
|
-
const
|
|
4175
|
+
mkdirSync(dirname(absOutput), { recursive: true });
|
|
4176
|
+
const stagingDir = mkdtempSync(join3(tmpdir(), "instructions-domain-export-"));
|
|
3832
4177
|
try {
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
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(
|
|
3856
|
-
rmSync(
|
|
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 {
|
|
3863
|
-
import {
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
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
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
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
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
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
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
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
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
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
|
|
4512
|
+
existsSync as existsSync5,
|
|
3934
4513
|
lstatSync,
|
|
3935
|
-
mkdirSync as
|
|
3936
|
-
readFileSync
|
|
4514
|
+
mkdirSync as mkdirSync2,
|
|
4515
|
+
readFileSync,
|
|
3937
4516
|
readdirSync,
|
|
3938
4517
|
statSync
|
|
3939
4518
|
} from "fs";
|
|
3940
|
-
import { dirname, isAbsolute, join as
|
|
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 (!
|
|
4072
|
-
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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(
|
|
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" ||
|
|
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(
|
|
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(
|
|
4433
|
-
const candidatePath = resolve4(
|
|
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(
|
|
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 =
|
|
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 =
|
|
4519
|
-
const previousSha256 = previousContent === null ? null :
|
|
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 (!
|
|
5196
|
+
if (!existsSync5(target))
|
|
4618
5197
|
return null;
|
|
4619
|
-
const previousContent =
|
|
4620
|
-
const previousSha256 =
|
|
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 (!
|
|
5264
|
+
if (!existsSync5(path))
|
|
4686
5265
|
return null;
|
|
4687
5266
|
try {
|
|
4688
|
-
const parsed = JSON.parse(
|
|
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 (!
|
|
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
|
|
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) =>
|
|
4743
|
-
const content =
|
|
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:
|
|
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 =
|
|
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 (
|
|
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 =
|
|
4825
|
-
if (
|
|
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 =
|
|
4837
|
-
if (!
|
|
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
|
|
4845
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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] ||
|
|
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
|
|
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 (!
|
|
5486
|
+
if (!existsSync6(path))
|
|
4908
5487
|
return null;
|
|
4909
|
-
const parsed = JSON.parse(
|
|
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") ??
|
|
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 =
|
|
5561
|
+
const scopeDir = join6(modulesDir, scope);
|
|
4983
5562
|
try {
|
|
4984
|
-
if (!
|
|
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(
|
|
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 (!
|
|
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 =
|
|
5658
|
+
const existing = existsSync6(path) ? readFileSync2(path, "utf8") : null;
|
|
5080
5659
|
if (existing !== content) {
|
|
5081
|
-
|
|
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 (!
|
|
5677
|
+
if (!existsSync6(path))
|
|
5099
5678
|
return null;
|
|
5100
|
-
return
|
|
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
|
|
6003
|
+
import { createHash as createHash4 } from "crypto";
|
|
5425
6004
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5426
6005
|
import {
|
|
5427
|
-
existsSync as
|
|
6006
|
+
existsSync as existsSync7,
|
|
5428
6007
|
lstatSync as lstatSync3,
|
|
5429
|
-
mkdirSync as
|
|
5430
|
-
readFileSync as
|
|
6008
|
+
mkdirSync as mkdirSync4,
|
|
6009
|
+
readFileSync as readFileSync3,
|
|
5431
6010
|
renameSync,
|
|
5432
|
-
rmSync as
|
|
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
|
|
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
|
|
5447
|
-
return
|
|
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 =
|
|
5463
|
-
if (!
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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) =>
|
|
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:
|
|
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(
|
|
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
|
-
|
|
6232
|
+
rmSync2(path, { force: true });
|
|
5663
6233
|
}
|
|
5664
6234
|
function writeAtomic(path, content, mode) {
|
|
5665
|
-
assertNoSymlinkAncestors2(
|
|
6235
|
+
assertNoSymlinkAncestors2(dirname4(path));
|
|
5666
6236
|
const tempPath = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
5667
6237
|
try {
|
|
5668
|
-
|
|
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) =>
|
|
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(
|
|
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
|
|
6391
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
5822
6392
|
|
|
5823
6393
|
// src/lib/package-version.ts
|
|
5824
|
-
import { existsSync as
|
|
5825
|
-
import { dirname as
|
|
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 =
|
|
6402
|
+
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
5833
6403
|
for (let i = 0;i < 8; i++) {
|
|
5834
|
-
const pkgPath =
|
|
5835
|
-
if (
|
|
5836
|
-
const pkg = JSON.parse(
|
|
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 =
|
|
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 (!
|
|
6481
|
+
if (!existsSync9(targetPath)) {
|
|
5912
6482
|
missingTargets += 1;
|
|
5913
6483
|
continue;
|
|
5914
6484
|
}
|
|
5915
|
-
const disk =
|
|
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
|
|
6008
|
-
import { existsSync as
|
|
6009
|
-
import { join as
|
|
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
|
|
6145
|
-
return
|
|
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 =
|
|
6155
|
-
if (!
|
|
6156
|
-
|
|
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 =
|
|
6159
|
-
const fragmentSha256 =
|
|
6728
|
+
const fragmentPath = join9(dir, filename);
|
|
6729
|
+
const fragmentSha256 = sha2565(content);
|
|
6160
6730
|
writeFileSync4(fragmentPath, content, "utf8");
|
|
6161
|
-
const manifestPath =
|
|
6731
|
+
const manifestPath = join9(dir, PROVIDER_CONTEXT_MANIFEST);
|
|
6162
6732
|
let manifest = { schema: PROVIDER_CONTEXT_SCHEMA, fragments: {} };
|
|
6163
6733
|
try {
|
|
6164
|
-
if (
|
|
6165
|
-
const parsed = JSON.parse(
|
|
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 (!
|
|
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
|
|
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 (!
|
|
6959
|
+
if (!existsSync11(path))
|
|
6390
6960
|
throw new Error(`Identity instruction export not found: ${path}`);
|
|
6391
|
-
const parsed = JSON.parse(
|
|
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 (!
|
|
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:
|
|
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 (!
|
|
7265
|
+
if (!existsSync11(abs)) {
|
|
6696
7266
|
console.error(chalk.red(`File not found: ${abs}`));
|
|
6697
7267
|
process.exit(1);
|
|
6698
7268
|
}
|
|
6699
|
-
const rawContent =
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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
|
|
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.
|
|
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("
|
|
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"] ||
|
|
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-
|
|
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-
|
|
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-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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"] ||
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
9031
|
+
existsSync11(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail2(`${k.rulesDir}/ not found`);
|
|
8268
9032
|
} else {
|
|
8269
|
-
|
|
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-
|
|
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 (!
|
|
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 =
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
9216
|
+
if (!existsSync11(absDir))
|
|
8453
9217
|
continue;
|
|
8454
9218
|
for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
8455
|
-
const abs =
|
|
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 (
|
|
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 (!
|
|
9255
|
+
if (!existsSync11(abs)) {
|
|
8492
9256
|
missing++;
|
|
8493
9257
|
continue;
|
|
8494
9258
|
}
|
|
8495
|
-
const disk =
|
|
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 (!
|
|
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)})`)}`);
|