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