@hasna/instructions 0.7.0 → 0.7.2
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 +52 -11
- package/dist/chunks/{apply-7svah2qm.js → apply-ps6jjt6k.js} +3 -3
- package/dist/chunks/{apply-jp13sa1e.js → apply-wj7wh5km.js} +3 -3
- package/dist/chunks/{index-1cnyjnd7.js → index-18j7x3xw.js} +2 -2
- package/dist/chunks/{index-3djthcm6.js → index-27az1qb1.js} +28 -1
- package/dist/chunks/{index-fwbs6tma.js → index-ape24zc0.js} +2 -2
- package/dist/chunks/{index-bzyxvwcd.js → index-ek15201v.js} +2 -2
- package/dist/chunks/{index-arrrx0ed.js → index-mvckkf15.js} +39 -20
- package/dist/chunks/{index-7m4j8a33.js → index-n6nh6f5r.js} +235 -20
- package/dist/chunks/{index-4pfq42t5.js → index-pq18fdbj.js} +146 -32
- package/dist/chunks/{index-n3fjspr0.js → index-preyvj8n.js} +28 -1
- package/dist/chunks/{index-z3ma4aqk.js → index-y8410ve0.js} +3 -3
- package/dist/chunks/{local-5wvv5xv3.js → local-1c1qxzvx.js} +155 -4
- package/dist/chunks/{local-zxa5dpf4.js → local-hc67bynj.js} +155 -4
- package/dist/chunks/{s3-backup-n2z4fwyh.js → s3-backup-hxe3zpfz.js} +1 -1
- package/dist/chunks/{s3-object-store-90tgbh6k.js → s3-object-store-n0s82swt.js} +1 -1
- package/dist/chunks/{sync-r8t7zsp3.js → sync-12m3vs6g.js} +4 -4
- package/dist/chunks/{sync-2075deve.js → sync-azd9t4p8.js} +4 -4
- package/dist/chunks/{template-wgnzdn7h.js → template-e4qrxs8p.js} +1 -1
- package/dist/chunks/{template-fsfxpe4p.js → template-nhn124zd.js} +1 -1
- package/dist/cli/index.js +860 -181
- package/dist/data/config-store.d.ts +22 -6
- 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.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +940 -134
- package/dist/lib/asset-plan.d.ts.map +1 -1
- package/dist/lib/codewith-shared-todos-storage-standard.d.ts.map +1 -1
- package/dist/lib/compact-output.d.ts +2 -18
- package/dist/lib/compact-output.d.ts.map +1 -1
- package/dist/lib/dangerous-operation-guard-standard.d.ts.map +1 -1
- package/dist/lib/export.d.ts.map +1 -1
- package/dist/lib/global-agent-rules-standard.d.ts.map +1 -1
- package/dist/lib/global-source-coverage.d.ts +12 -7
- package/dist/lib/global-source-coverage.d.ts.map +1 -1
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/instruction-graph.d.ts +1 -1
- package/dist/lib/instruction-graph.d.ts.map +1 -1
- package/dist/lib/instruction-source-policy.d.ts +6 -0
- package/dist/lib/instruction-source-policy.d.ts.map +1 -0
- package/dist/lib/platform-profiles.d.ts.map +1 -1
- package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
- package/dist/lib/session-apply.d.ts +24 -0
- package/dist/lib/session-apply.d.ts.map +1 -1
- package/dist/lib/session-refresh.d.ts +30 -0
- package/dist/lib/session-refresh.d.ts.map +1 -0
- package/dist/lib/session-render.d.ts +34 -1
- package/dist/lib/session-render.d.ts.map +1 -1
- package/dist/lib/transforms.d.ts +1 -0
- package/dist/lib/transforms.d.ts.map +1 -1
- package/dist/mcp/index.js +23 -22
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/sdk/index.js +46 -3
- package/dist/sdk/v1-client.d.ts +10 -3
- package/dist/sdk/v1-client.d.ts.map +1 -1
- package/dist/sdk/v1.generated.d.ts +52 -2
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +215 -15
- package/dist/server/openapi.d.ts +240 -3
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/cloud-store.d.ts +2 -15
- package/dist/storage/cloud-store.d.ts.map +1 -1
- package/dist/storage/index.js +39 -20
- package/dist/storage/s3-object-store.d.ts.map +1 -1
- package/dist/types/index.d.ts +49 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/cli/index.js
CHANGED
|
@@ -9,31 +9,39 @@ import {
|
|
|
9
9
|
syncKnown,
|
|
10
10
|
syncProject,
|
|
11
11
|
syncToDisk
|
|
12
|
-
} from "../chunks/index-
|
|
12
|
+
} from "../chunks/index-18j7x3xw.js";
|
|
13
13
|
import {
|
|
14
14
|
applyConfigsWithReport,
|
|
15
15
|
compactPathForConfigHome,
|
|
16
16
|
expandPath,
|
|
17
|
-
normalizeTargetPath
|
|
18
|
-
|
|
17
|
+
normalizeTargetPath,
|
|
18
|
+
sessionRenderOwnsPath
|
|
19
|
+
} from "../chunks/index-y8410ve0.js";
|
|
19
20
|
import {
|
|
21
|
+
CODEWITH_NATIVE_IMPORTS_ENV,
|
|
20
22
|
CloudConfigStore,
|
|
21
23
|
DEFAULT_LIST_LIMIT,
|
|
22
24
|
PROJECT_CONTEXT_MAX_INPUT_BYTES,
|
|
23
25
|
ProjectContextError,
|
|
24
26
|
SESSION_INSTRUCTION_LAYERS,
|
|
25
27
|
SESSION_MANAGED_INPUT_MAX_BYTES,
|
|
28
|
+
SESSION_RENDERER_OWNER_ID,
|
|
26
29
|
SESSION_RENDER_MANAGED_MARKER,
|
|
30
|
+
SESSION_RENDER_MANIFEST_RELATIVE_PATH,
|
|
27
31
|
SESSION_RENDER_SCHEMA,
|
|
28
32
|
SESSION_RENDER_TOOLS,
|
|
29
33
|
applyProjectContext,
|
|
34
|
+
assertInstructionSource,
|
|
30
35
|
detectClaudeAuthorityConflicts,
|
|
31
36
|
detectCursorAuthorityConflicts,
|
|
32
37
|
detectMachineContext,
|
|
33
38
|
ensureCodewithSharedTodosStorageStandardConfig,
|
|
34
39
|
ensureGlobalAgentRulesStandardConfig,
|
|
40
|
+
exports_external,
|
|
35
41
|
formatCliError,
|
|
36
42
|
getSessionRenderSnapshotDir,
|
|
43
|
+
instructionSourceRejection,
|
|
44
|
+
isRetiredInstructionSource,
|
|
37
45
|
isRetiredOrUnsupportedConfigAgent,
|
|
38
46
|
normalizeProfileAssetBinding,
|
|
39
47
|
normalizeProfileConfigBinding,
|
|
@@ -61,11 +69,14 @@ import {
|
|
|
61
69
|
truncateText,
|
|
62
70
|
withProjectContextSessionGuard,
|
|
63
71
|
writeProjectContextCoordinatedFile
|
|
64
|
-
} from "../chunks/index-
|
|
72
|
+
} from "../chunks/index-n6nh6f5r.js";
|
|
65
73
|
import {
|
|
74
|
+
ConfigNotFoundError,
|
|
66
75
|
INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
ProfileNotFoundError,
|
|
77
|
+
extractTemplateVars,
|
|
78
|
+
validateExpectedConfigVersion
|
|
79
|
+
} from "../chunks/index-preyvj8n.js";
|
|
69
80
|
import {
|
|
70
81
|
slugify
|
|
71
82
|
} from "../chunks/index-ma2xmp6c.js";
|
|
@@ -3784,9 +3795,9 @@ var {
|
|
|
3784
3795
|
|
|
3785
3796
|
// src/cli/index.tsx
|
|
3786
3797
|
import chalk from "chalk";
|
|
3787
|
-
import { existsSync as existsSync11, lstatSync as
|
|
3798
|
+
import { existsSync as existsSync11, lstatSync as lstatSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync8, readSync, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync5, writeSync } from "fs";
|
|
3788
3799
|
import { homedir as homedir4 } from "os";
|
|
3789
|
-
import { basename, dirname as
|
|
3800
|
+
import { basename as basename2, dirname as dirname7, join as join11, resolve as resolve7 } from "path";
|
|
3790
3801
|
|
|
3791
3802
|
// src/lib/config-target-identity.ts
|
|
3792
3803
|
function findConfigsByTargetPath(configs, targetPath) {
|
|
@@ -3878,7 +3889,12 @@ function logicalAssetLocator(locator, idToSlug) {
|
|
|
3878
3889
|
}
|
|
3879
3890
|
function logicalCollections(domain, options) {
|
|
3880
3891
|
const configs = [...domain.configs].sort((left, right) => compareText(left.slug, right.slug));
|
|
3881
|
-
const snapshots =
|
|
3892
|
+
const snapshots = domain.config_snapshots.map((snapshot) => ({
|
|
3893
|
+
config_slug: snapshot.config_slug,
|
|
3894
|
+
version: snapshot.version,
|
|
3895
|
+
content: snapshot.content,
|
|
3896
|
+
...options.includeOperationalTimestamps ? { created_at: snapshot.created_at } : {}
|
|
3897
|
+
})).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 ?? "" : ""));
|
|
3882
3898
|
const profiles = [...domain.profiles].sort((left, right) => compareText(left.slug, right.slug));
|
|
3883
3899
|
const configBindings = [...domain.profile_config_bindings].sort((left, right) => compareText(left.profile_slug, right.profile_slug) || left.sort_order - right.sort_order || compareText(left.config_slug, right.config_slug));
|
|
3884
3900
|
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));
|
|
@@ -3907,12 +3923,7 @@ function logicalCollections(domain, options) {
|
|
|
3907
3923
|
synced_at: config.synced_at
|
|
3908
3924
|
} : {}
|
|
3909
3925
|
})),
|
|
3910
|
-
config_snapshots: snapshots
|
|
3911
|
-
config_slug: snapshot.config_slug,
|
|
3912
|
-
version: snapshot.version,
|
|
3913
|
-
content: snapshot.content,
|
|
3914
|
-
...options.includeOperationalTimestamps ? { created_at: snapshot.created_at } : {}
|
|
3915
|
-
})),
|
|
3926
|
+
config_snapshots: snapshots,
|
|
3916
3927
|
profiles: profiles.map((profile) => ({
|
|
3917
3928
|
slug: profile.slug,
|
|
3918
3929
|
name: profile.name,
|
|
@@ -4036,10 +4047,11 @@ function validateInstructionsDomainArchive(value) {
|
|
|
4036
4047
|
assertUnique(typed.configs.map((row) => row.slug), "config slug");
|
|
4037
4048
|
assertUnique(typed.profiles.map((row) => row.slug), "profile slug");
|
|
4038
4049
|
assertUnique(typed.machines.map((row) => row.hostname), "machine hostname");
|
|
4039
|
-
assertUnique(typed.config_snapshots.map((row) =>
|
|
4050
|
+
assertUnique(typed.config_snapshots.map((row) => row.id), "config snapshot id");
|
|
4040
4051
|
assertUnique(typed.profile_config_bindings.map((row) => `${row.profile_slug}:${row.config_slug}`), "profile config binding");
|
|
4041
4052
|
assertUnique(typed.profile_asset_bindings.map((row) => `${row.profile_slug}:${row.binding.assetKey}`), "profile asset binding");
|
|
4042
|
-
const
|
|
4053
|
+
const configsBySlug = new Map(typed.configs.map((row) => [row.slug, row]));
|
|
4054
|
+
const configSlugs = new Set(configsBySlug.keys());
|
|
4043
4055
|
const profileSlugs = new Set(typed.profiles.map((row) => row.slug));
|
|
4044
4056
|
for (const config of typed.configs) {
|
|
4045
4057
|
assertTimestamp(config.created_at, `config ${config.slug} created_at`);
|
|
@@ -4047,24 +4059,15 @@ function validateInstructionsDomainArchive(value) {
|
|
|
4047
4059
|
assertTimestamp(config.synced_at, `config ${config.slug} synced_at`, true);
|
|
4048
4060
|
if (!Number.isSafeInteger(config.version) || config.version < 1)
|
|
4049
4061
|
throw new Error(`Invalid v2 archive: config ${config.slug} has invalid version`);
|
|
4050
|
-
const snapshots = typed.config_snapshots.filter((row) => row.config_slug === config.slug).sort((a, b) => a.version - b.version);
|
|
4051
|
-
if (snapshots.some((row) => !Number.isSafeInteger(row.version) || row.version < 1 || row.version > config.version)) {
|
|
4052
|
-
throw new Error(`Invalid v2 archive: config ${config.slug} has an invalid snapshot version`);
|
|
4053
|
-
}
|
|
4054
|
-
if (snapshots.length) {
|
|
4055
|
-
const first = snapshots[0].version;
|
|
4056
|
-
const expected = Array.from({ length: config.version - first + 1 }, (_, index) => first + index);
|
|
4057
|
-
if (canonicalDomainJson(snapshots.map((row) => row.version)) !== canonicalDomainJson(expected) || snapshots.at(-1).version !== config.version) {
|
|
4058
|
-
throw new Error(`Invalid v2 archive: snapshots for ${config.slug} must be a contiguous suffix ending at version ${config.version}`);
|
|
4059
|
-
}
|
|
4060
|
-
if (snapshots.at(-1).content !== config.content) {
|
|
4061
|
-
throw new Error(`Invalid v2 archive: latest snapshot content differs from config ${config.slug}`);
|
|
4062
|
-
}
|
|
4063
|
-
}
|
|
4064
4062
|
}
|
|
4065
4063
|
for (const row of typed.config_snapshots) {
|
|
4066
|
-
|
|
4064
|
+
const config = configsBySlug.get(row.config_slug);
|
|
4065
|
+
if (!config) {
|
|
4067
4066
|
throw new Error(`Invalid v2 archive: snapshot references missing config ${row.config_slug}`);
|
|
4067
|
+
}
|
|
4068
|
+
if (!Number.isSafeInteger(row.version) || row.version < 1 || row.version > config.version) {
|
|
4069
|
+
throw new Error(`Invalid v2 archive: config ${row.config_slug} has an invalid snapshot version`);
|
|
4070
|
+
}
|
|
4068
4071
|
assertTimestamp(row.created_at, `snapshot ${row.config_slug}@${row.version} created_at`);
|
|
4069
4072
|
}
|
|
4070
4073
|
for (const profile of typed.profiles) {
|
|
@@ -4318,14 +4321,11 @@ function configInput(config, content) {
|
|
|
4318
4321
|
};
|
|
4319
4322
|
}
|
|
4320
4323
|
async function restoreNewConfig(store, archived, domain) {
|
|
4321
|
-
const snapshots = domain.config_snapshots.filter((row) => row.config_slug === archived.slug).sort((left, right) => left.version - right.version);
|
|
4322
|
-
|
|
4323
|
-
let content = byVersion.get(1) ?? snapshots[0]?.content ?? archived.content;
|
|
4324
|
-
let restored = await store.createConfig(configInput(archived, content));
|
|
4324
|
+
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));
|
|
4325
|
+
let restored = await store.createConfig(configInput(archived, archived.content));
|
|
4325
4326
|
for (let version = 2;version <= archived.version; version++) {
|
|
4326
|
-
content = byVersion.get(version) ?? (version === archived.version ? archived.content : content);
|
|
4327
4327
|
restored = await store.updateConfig(restored.id, {
|
|
4328
|
-
content,
|
|
4328
|
+
content: archived.content,
|
|
4329
4329
|
...version === archived.version ? {
|
|
4330
4330
|
name: archived.name,
|
|
4331
4331
|
kind: archived.kind,
|
|
@@ -4341,9 +4341,12 @@ async function restoreNewConfig(store, archived, domain) {
|
|
|
4341
4341
|
} : {}
|
|
4342
4342
|
});
|
|
4343
4343
|
}
|
|
4344
|
-
await store.pruneSnapshots(restored.id,
|
|
4345
|
-
|
|
4346
|
-
|
|
4344
|
+
await store.pruneSnapshots(restored.id, 0);
|
|
4345
|
+
for (const snapshot of snapshots) {
|
|
4346
|
+
await store.createSnapshot(restored.id, snapshot.content, snapshot.version);
|
|
4347
|
+
}
|
|
4348
|
+
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));
|
|
4349
|
+
const expectedSnapshots = snapshots.map(({ version, content: snapshotContent }) => ({ version, content: snapshotContent })).sort((left, right) => left.version - right.version || compareText2(left.content, right.content));
|
|
4347
4350
|
if (restored.version !== archived.version || restored.content !== archived.content || canonicalDomainJson(restoredSnapshots) !== canonicalDomainJson(expectedSnapshots)) {
|
|
4348
4351
|
throw new Error(`restored config or snapshot history did not match archive for ${archived.slug}`);
|
|
4349
4352
|
}
|
|
@@ -4538,14 +4541,20 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4538
4541
|
const targetHome = assertSafeTargetHome(plan.targetHome);
|
|
4539
4542
|
assertClaudeAuthorityStillClear(plan, targetHome, options.ownedClaudeAuthorities);
|
|
4540
4543
|
const payloadFiles = [...plan.files, ...plan.assetFiles ?? []];
|
|
4541
|
-
const files = [...payloadFiles, plan.manifestFile];
|
|
4542
4544
|
const manifestPath = resolvePlannedFilePath(plan, plan.manifestFile, targetHome);
|
|
4545
|
+
assertManifestPrecondition(manifestPath, targetHome, options.expectedManifestSha256);
|
|
4543
4546
|
const previousManifest = readPreviousManifest(manifestPath);
|
|
4544
4547
|
const previousHashes = previousManifest ? new Map(previousManifest.files.map((file) => [file.relativePath, file.sha256])) : new Map;
|
|
4548
|
+
const adoptions = validateFileAdoptions(plan, targetHome, previousHashes, options);
|
|
4549
|
+
const adoptedHashes = new Map(adoptions.map((entry) => [entry.relativePath, entry.preimageSha256]));
|
|
4550
|
+
const reconciliations = validateFileReconciliations(plan, targetHome, previousManifest, options);
|
|
4551
|
+
const reconciledHashes = new Map(reconciliations.map((entry) => [entry.relativePath, entry.preimageSha256]));
|
|
4552
|
+
const manifestFile = manifestWithFileProvenance(plan, previousManifest, adoptions, reconciliations);
|
|
4553
|
+
const files = [...payloadFiles, manifestFile];
|
|
4545
4554
|
const currentRelativePaths = new Set(files.map((file) => file.relativePath));
|
|
4546
4555
|
const drift = checkSessionRenderDrift(targetHome, manifestPath);
|
|
4547
4556
|
const results = [
|
|
4548
|
-
...files.map((file) => planFileResult(plan, file, targetHome, previousHashes, previousManifest, options)),
|
|
4557
|
+
...files.map((file) => planFileResult(plan, file, targetHome, previousHashes, previousManifest, options, adoptedHashes, reconciledHashes)),
|
|
4549
4558
|
...planStaleFileResults(plan, targetHome, previousManifest, currentRelativePaths, options)
|
|
4550
4559
|
];
|
|
4551
4560
|
assertNotSilentManagedWipeout(plan, results);
|
|
@@ -4568,7 +4577,9 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4568
4577
|
skippedSources: plan.manifest.skippedSources,
|
|
4569
4578
|
files: results,
|
|
4570
4579
|
conflicts,
|
|
4571
|
-
drift
|
|
4580
|
+
drift,
|
|
4581
|
+
adoptions,
|
|
4582
|
+
reconciliations
|
|
4572
4583
|
};
|
|
4573
4584
|
}
|
|
4574
4585
|
let snapshotPath = null;
|
|
@@ -4581,10 +4592,12 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4581
4592
|
if (!options.dryRun) {
|
|
4582
4593
|
const allowPortableFallback = coordination === null;
|
|
4583
4594
|
const forcePortableFileOps = options.test_hooks?.force_portable_file_ops ?? false;
|
|
4595
|
+
assertManifestPrecondition(manifestPath, targetHome, options.expectedManifestSha256);
|
|
4584
4596
|
ensureSessionTargetHome(targetHome);
|
|
4585
|
-
rollback = writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps);
|
|
4597
|
+
rollback = writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps, adoptions, reconciliations);
|
|
4586
4598
|
snapshotPath = rollback.snapshotPath;
|
|
4587
4599
|
options.test_hooks?.before_apply_writes?.({ plan, results });
|
|
4600
|
+
assertManifestPrecondition(manifestPath, targetHome, options.expectedManifestSha256);
|
|
4588
4601
|
const resultsByPath = new Map(results.map((result) => [result.path, result]));
|
|
4589
4602
|
for (const file of payloadFiles) {
|
|
4590
4603
|
applyPlannedFile(plan, file, targetHome, resultsByPath, coordination, allowPortableFallback, forcePortableFileOps);
|
|
@@ -4604,7 +4617,7 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4604
4617
|
});
|
|
4605
4618
|
coordination?.assert_held();
|
|
4606
4619
|
}
|
|
4607
|
-
applyPlannedFile(plan,
|
|
4620
|
+
applyPlannedFile(plan, manifestFile, targetHome, resultsByPath, coordination, allowPortableFallback, forcePortableFileOps);
|
|
4608
4621
|
}
|
|
4609
4622
|
return {
|
|
4610
4623
|
dryRun: options.dryRun ?? false,
|
|
@@ -4618,9 +4631,171 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4618
4631
|
skippedSources: plan.manifest.skippedSources,
|
|
4619
4632
|
files: results,
|
|
4620
4633
|
conflicts,
|
|
4621
|
-
drift
|
|
4634
|
+
drift,
|
|
4635
|
+
adoptions,
|
|
4636
|
+
reconciliations
|
|
4622
4637
|
};
|
|
4623
4638
|
}
|
|
4639
|
+
function assertManifestPrecondition(path, targetHome, expected) {
|
|
4640
|
+
if (expected === undefined)
|
|
4641
|
+
return;
|
|
4642
|
+
if (!/^[a-f0-9]{64}$/.test(expected)) {
|
|
4643
|
+
throw new SessionApplyError("Expected session manifest SHA-256 must contain 64 lowercase hexadecimal characters.");
|
|
4644
|
+
}
|
|
4645
|
+
if (currentSessionFileHash(path, targetHome) !== expected) {
|
|
4646
|
+
throw new SessionApplyError("Session manifest SHA-256 precondition failed; reload the manifest and resolve a new plan.");
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
function validateFileAdoptions(plan, targetHome, previousHashes, options) {
|
|
4650
|
+
const requests = options.adoptFiles ?? [];
|
|
4651
|
+
if (!Array.isArray(requests))
|
|
4652
|
+
throw new SessionApplyError("Session file adoptions must be an array.");
|
|
4653
|
+
if (requests.length > 0 && options.force) {
|
|
4654
|
+
throw new SessionApplyError("Exact file adoption cannot be combined with force.");
|
|
4655
|
+
}
|
|
4656
|
+
const seen = new Set;
|
|
4657
|
+
return requests.map((request) => {
|
|
4658
|
+
if (!request || typeof request.relativePath !== "string" || typeof request.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(request.sha256)) {
|
|
4659
|
+
throw new SessionApplyError("Each file adoption requires a plan-relative path and a 64-character lowercase SHA-256.");
|
|
4660
|
+
}
|
|
4661
|
+
if (seen.has(request.relativePath)) {
|
|
4662
|
+
throw new SessionApplyError(`Duplicate file adoption target: ${request.relativePath}`);
|
|
4663
|
+
}
|
|
4664
|
+
seen.add(request.relativePath);
|
|
4665
|
+
const candidates = plan.files.filter((file2) => file2.relativePath === request.relativePath);
|
|
4666
|
+
if (candidates.length !== 1 || candidates[0].role === "manifest") {
|
|
4667
|
+
throw new SessionApplyError(`File adoption target is not a unique planned instruction output: ${request.relativePath}`);
|
|
4668
|
+
}
|
|
4669
|
+
const file = candidates[0];
|
|
4670
|
+
const path = resolvePlannedFilePath(plan, file, targetHome);
|
|
4671
|
+
if (previousHashes.has(file.relativePath) || sessionRenderOwnsPath(path)) {
|
|
4672
|
+
throw new SessionApplyError(`File adoption target is already managed: ${request.relativePath}`);
|
|
4673
|
+
}
|
|
4674
|
+
const observedSha256 = readExactFilePreimage(path, targetHome, request, "adoption");
|
|
4675
|
+
return {
|
|
4676
|
+
path,
|
|
4677
|
+
relativePath: file.relativePath,
|
|
4678
|
+
preimageSha256: observedSha256,
|
|
4679
|
+
renderedSha256: file.sha256,
|
|
4680
|
+
sourceIds: [...file.sourceIds]
|
|
4681
|
+
};
|
|
4682
|
+
});
|
|
4683
|
+
}
|
|
4684
|
+
function readExactFilePreimage(path, targetHome, request, operation) {
|
|
4685
|
+
if (currentSessionFileHash(path, targetHome) === null) {
|
|
4686
|
+
throw new SessionApplyError(`File ${operation} target does not exist: ${request.relativePath}`);
|
|
4687
|
+
}
|
|
4688
|
+
const bytes = readFileSync(path);
|
|
4689
|
+
if (!bytes.equals(Buffer.from(bytes.toString("utf8"), "utf8"))) {
|
|
4690
|
+
throw new SessionApplyError(`File ${operation} target must contain losslessly restorable UTF-8: ${request.relativePath}`);
|
|
4691
|
+
}
|
|
4692
|
+
const observedSha256 = createHash3("sha256").update(bytes).digest("hex");
|
|
4693
|
+
if (observedSha256 !== request.sha256) {
|
|
4694
|
+
throw new SessionApplyError(`File ${operation} SHA-256 precondition failed: ${request.relativePath}`);
|
|
4695
|
+
}
|
|
4696
|
+
return observedSha256;
|
|
4697
|
+
}
|
|
4698
|
+
function validateFileReconciliations(plan, targetHome, previousManifest, options) {
|
|
4699
|
+
const requests = options.reconcileFiles ?? [];
|
|
4700
|
+
if (!Array.isArray(requests))
|
|
4701
|
+
throw new SessionApplyError("Session file reconciliations must be an array.");
|
|
4702
|
+
if (requests.length === 0)
|
|
4703
|
+
return [];
|
|
4704
|
+
if (options.force)
|
|
4705
|
+
throw new SessionApplyError("Exact file reconciliation cannot be combined with force.");
|
|
4706
|
+
if (options.expectedManifestSha256 === undefined) {
|
|
4707
|
+
throw new SessionApplyError("Exact file reconciliation requires an expected manifest SHA-256 precondition.");
|
|
4708
|
+
}
|
|
4709
|
+
if (!previousManifest || previousManifest.targetOwner?.writer?.id !== SESSION_RENDERER_OWNER_ID || previousManifest.tool !== plan.tool || previousManifest.targetHome !== targetHome) {
|
|
4710
|
+
throw new SessionApplyError("File reconciliation requires this renderer's manifest for the same tool and target home.");
|
|
4711
|
+
}
|
|
4712
|
+
const seen = new Set;
|
|
4713
|
+
return requests.map((request) => {
|
|
4714
|
+
if (!request || typeof request.relativePath !== "string" || typeof request.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(request.sha256)) {
|
|
4715
|
+
throw new SessionApplyError("Each file reconciliation requires a plan-relative path and a 64-character lowercase SHA-256.");
|
|
4716
|
+
}
|
|
4717
|
+
if (seen.has(request.relativePath)) {
|
|
4718
|
+
throw new SessionApplyError(`Duplicate file reconciliation target: ${request.relativePath}`);
|
|
4719
|
+
}
|
|
4720
|
+
seen.add(request.relativePath);
|
|
4721
|
+
const candidates = plan.files.filter((file2) => file2.relativePath === request.relativePath);
|
|
4722
|
+
if (candidates.length !== 1 || candidates[0].role === "manifest") {
|
|
4723
|
+
throw new SessionApplyError(`File reconciliation target is not a unique planned instruction output: ${request.relativePath}`);
|
|
4724
|
+
}
|
|
4725
|
+
const file = candidates[0];
|
|
4726
|
+
const path = resolvePlannedFilePath(plan, file, targetHome);
|
|
4727
|
+
const owned = previousManifest.files.filter((entry) => entry.relativePath === file.relativePath);
|
|
4728
|
+
if (owned.length !== 1 || owned[0].path !== path || owned[0].role !== file.role || !/^[a-f0-9]{64}$/.test(owned[0].sha256)) {
|
|
4729
|
+
throw new SessionApplyError(`File reconciliation target is not owned by the observed manifest: ${request.relativePath}`);
|
|
4730
|
+
}
|
|
4731
|
+
const observedSha256 = readExactFilePreimage(path, targetHome, request, "reconciliation");
|
|
4732
|
+
if (observedSha256 === owned[0].sha256) {
|
|
4733
|
+
throw new SessionApplyError(`File reconciliation target has no managed drift: ${request.relativePath}`);
|
|
4734
|
+
}
|
|
4735
|
+
if (observedSha256 === file.sha256) {
|
|
4736
|
+
throw new SessionApplyError(`File reconciliation target already matches the planned output: ${request.relativePath}`);
|
|
4737
|
+
}
|
|
4738
|
+
return {
|
|
4739
|
+
path,
|
|
4740
|
+
relativePath: file.relativePath,
|
|
4741
|
+
preimageSha256: observedSha256,
|
|
4742
|
+
renderedSha256: file.sha256,
|
|
4743
|
+
previousManagedSha256: owned[0].sha256,
|
|
4744
|
+
sourceIds: [...file.sourceIds]
|
|
4745
|
+
};
|
|
4746
|
+
});
|
|
4747
|
+
}
|
|
4748
|
+
function manifestWithFileProvenance(plan, previousManifest, adoptions, reconciliations) {
|
|
4749
|
+
if (adoptions.length === 0 && previousManifest?.adoptions === undefined && reconciliations.length === 0 && previousManifest?.reconciliations === undefined)
|
|
4750
|
+
return plan.manifestFile;
|
|
4751
|
+
if (previousManifest?.adoptions !== undefined && !Array.isArray(previousManifest.adoptions)) {
|
|
4752
|
+
throw new SessionApplyError("Previous session manifest adoption provenance is invalid.");
|
|
4753
|
+
}
|
|
4754
|
+
const entries = new Map;
|
|
4755
|
+
for (const entry of [
|
|
4756
|
+
...previousManifest?.adoptions ?? [],
|
|
4757
|
+
...adoptions.map(({ path: _path, ...receipt }) => receipt)
|
|
4758
|
+
]) {
|
|
4759
|
+
if (!entry || typeof entry.relativePath !== "string" || !/^[a-f0-9]{64}$/.test(entry.preimageSha256) || !/^[a-f0-9]{64}$/.test(entry.renderedSha256) || !Array.isArray(entry.sourceIds) || entry.sourceIds.some((id) => typeof id !== "string")) {
|
|
4760
|
+
throw new SessionApplyError("Previous session manifest adoption provenance is invalid.");
|
|
4761
|
+
}
|
|
4762
|
+
resolveManifestRelativePath(entry.relativePath, plan.targetHome);
|
|
4763
|
+
const previous = entries.get(entry.relativePath);
|
|
4764
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(entry)) {
|
|
4765
|
+
throw new SessionApplyError(`Conflicting session file adoption provenance: ${entry.relativePath}`);
|
|
4766
|
+
}
|
|
4767
|
+
entries.set(entry.relativePath, entry);
|
|
4768
|
+
}
|
|
4769
|
+
if (previousManifest?.reconciliations !== undefined && !Array.isArray(previousManifest.reconciliations)) {
|
|
4770
|
+
throw new SessionApplyError("Previous session manifest reconciliation provenance is invalid.");
|
|
4771
|
+
}
|
|
4772
|
+
const reconciledEntries = new Map;
|
|
4773
|
+
for (const entry of [
|
|
4774
|
+
...previousManifest?.reconciliations ?? [],
|
|
4775
|
+
...reconciliations.map(({ path: _path, ...receipt }) => receipt)
|
|
4776
|
+
]) {
|
|
4777
|
+
if (!entry || typeof entry.relativePath !== "string" || !/^[a-f0-9]{64}$/.test(entry.preimageSha256) || !/^[a-f0-9]{64}$/.test(entry.renderedSha256) || !/^[a-f0-9]{64}$/.test(entry.previousManagedSha256) || !Array.isArray(entry.sourceIds) || entry.sourceIds.some((id) => typeof id !== "string")) {
|
|
4778
|
+
throw new SessionApplyError("Previous session manifest reconciliation provenance is invalid.");
|
|
4779
|
+
}
|
|
4780
|
+
resolveManifestRelativePath(entry.relativePath, plan.targetHome);
|
|
4781
|
+
const canonical = {
|
|
4782
|
+
relativePath: entry.relativePath,
|
|
4783
|
+
preimageSha256: entry.preimageSha256,
|
|
4784
|
+
renderedSha256: entry.renderedSha256,
|
|
4785
|
+
previousManagedSha256: entry.previousManagedSha256,
|
|
4786
|
+
sourceIds: [...entry.sourceIds]
|
|
4787
|
+
};
|
|
4788
|
+
reconciledEntries.set(JSON.stringify(canonical), canonical);
|
|
4789
|
+
}
|
|
4790
|
+
const manifest = {
|
|
4791
|
+
...plan.manifest,
|
|
4792
|
+
...adoptions.length > 0 || previousManifest?.adoptions !== undefined ? { adoptions: [...entries.values()] } : {},
|
|
4793
|
+
...reconciliations.length > 0 || previousManifest?.reconciliations !== undefined ? { reconciliations: [...reconciledEntries.values()] } : {}
|
|
4794
|
+
};
|
|
4795
|
+
const content = `${JSON.stringify(manifest, null, 2)}
|
|
4796
|
+
`;
|
|
4797
|
+
return { ...plan.manifestFile, content, sha256: sha2563(content) };
|
|
4798
|
+
}
|
|
4624
4799
|
function assertCursorAuthorityUnchanged(plan) {
|
|
4625
4800
|
if (plan.tool !== "cursor" || plan.targetKind === "blocked")
|
|
4626
4801
|
return;
|
|
@@ -5092,12 +5267,29 @@ function resolveSnapshotFilePath(relativePath, recordedPath, targetHome) {
|
|
|
5092
5267
|
}
|
|
5093
5268
|
return path;
|
|
5094
5269
|
}
|
|
5095
|
-
function planFileResult(plan, file, targetHome, previousHashes, previousManifest, options) {
|
|
5270
|
+
function planFileResult(plan, file, targetHome, previousHashes, previousManifest, options, adoptedHashes, reconciledHashes) {
|
|
5096
5271
|
const target = resolvePlannedFilePath(plan, file, targetHome);
|
|
5097
5272
|
const previousContent = existsSync5(target) ? readFileSync(target, "utf-8") : null;
|
|
5098
5273
|
const previousSha256 = previousContent === null ? null : sha2563(previousContent);
|
|
5099
5274
|
const previouslyManaged = isPreviouslyManaged(file, previousSha256, previousHashes, previousManifest);
|
|
5100
5275
|
const changed = previousContent !== file.content;
|
|
5276
|
+
const exactSha256 = adoptedHashes.get(file.relativePath) ?? reconciledHashes.get(file.relativePath);
|
|
5277
|
+
if (exactSha256 !== undefined) {
|
|
5278
|
+
const operation = adoptedHashes.has(file.relativePath) ? "adoption" : "reconciliation";
|
|
5279
|
+
if (previousSha256 !== exactSha256) {
|
|
5280
|
+
throw new SessionApplyError(`File ${operation} SHA-256 precondition failed: ${file.relativePath}`);
|
|
5281
|
+
}
|
|
5282
|
+
return {
|
|
5283
|
+
path: target,
|
|
5284
|
+
relativePath: file.relativePath,
|
|
5285
|
+
role: file.role,
|
|
5286
|
+
action: "update",
|
|
5287
|
+
changed,
|
|
5288
|
+
previousSha256,
|
|
5289
|
+
newSha256: file.sha256,
|
|
5290
|
+
reason: `exact observed SHA-256 ${operation}`
|
|
5291
|
+
};
|
|
5292
|
+
}
|
|
5101
5293
|
if (previousContent !== null && !options.force && !previouslyManaged) {
|
|
5102
5294
|
return {
|
|
5103
5295
|
path: target,
|
|
@@ -5317,9 +5509,13 @@ function requiredPreviousHash(result) {
|
|
|
5317
5509
|
}
|
|
5318
5510
|
return result.previousSha256;
|
|
5319
5511
|
}
|
|
5320
|
-
function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps) {
|
|
5321
|
-
const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").
|
|
5512
|
+
function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps, adoptions, reconciliations) {
|
|
5513
|
+
const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").map((result) => {
|
|
5514
|
+
assertExpectedSessionFileHash(result.path, targetHome, result.previousSha256);
|
|
5322
5515
|
const content = readFileSync(result.path, "utf-8");
|
|
5516
|
+
if (sha2563(content) !== result.previousSha256) {
|
|
5517
|
+
throw new SessionApplyError(`Session snapshot preimage changed after planning: ${result.relativePath}`);
|
|
5518
|
+
}
|
|
5323
5519
|
return {
|
|
5324
5520
|
path: result.path,
|
|
5325
5521
|
relativePath: result.relativePath,
|
|
@@ -5360,13 +5556,19 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
5360
5556
|
manifestPath,
|
|
5361
5557
|
previousManifest,
|
|
5362
5558
|
files: existingFiles,
|
|
5363
|
-
afterFiles
|
|
5559
|
+
afterFiles,
|
|
5560
|
+
...adoptions.length > 0 ? { adoptions } : {},
|
|
5561
|
+
...reconciliations.length > 0 ? { reconciliations } : {}
|
|
5364
5562
|
};
|
|
5563
|
+
const snapshotContent = `${JSON.stringify(snapshot, null, 2)}
|
|
5564
|
+
`;
|
|
5565
|
+
if ((adoptions.length > 0 || reconciliations.length > 0) && Buffer.byteLength(snapshotContent, "utf8") > 32 * 1024 * 1024) {
|
|
5566
|
+
throw new SessionApplyError("Exact file preimage snapshot exceeds the 32 MiB restore limit.");
|
|
5567
|
+
}
|
|
5365
5568
|
coordination?.assert_held();
|
|
5366
5569
|
writeProjectContextCoordinatedFile({
|
|
5367
5570
|
path: snapshotPath,
|
|
5368
|
-
content:
|
|
5369
|
-
`,
|
|
5571
|
+
content: snapshotContent,
|
|
5370
5572
|
workspace_root: sessionRenderSnapshotWorkspaceRoot(targetHome),
|
|
5371
5573
|
default_mode: 384,
|
|
5372
5574
|
expected_hash: null,
|
|
@@ -5424,11 +5626,15 @@ function sha2563(content) {
|
|
|
5424
5626
|
return createHash3("sha256").update(content).digest("hex");
|
|
5425
5627
|
}
|
|
5426
5628
|
|
|
5629
|
+
// src/lib/session-refresh.ts
|
|
5630
|
+
import { createHash as createHash4 } from "crypto";
|
|
5631
|
+
import { closeSync, constants, fstatSync, lstatSync as lstatSync3, openSync, readFileSync as readFileSync3 } from "fs";
|
|
5632
|
+
import { dirname as dirname4, join as join7, parse as parse2, resolve as resolve5 } from "path";
|
|
5633
|
+
|
|
5427
5634
|
// src/lib/global-source-coverage.ts
|
|
5428
5635
|
var GLOBAL_SOURCE_SLUG_PREFIX = "global-";
|
|
5429
|
-
var RETIRED_GLOBAL_SOURCE_TAG = "retired-global-source";
|
|
5430
5636
|
function expectedGlobalSourceSlugs(registryConfigs) {
|
|
5431
|
-
return registryConfigs.filter((c) => c.slug.startsWith(GLOBAL_SOURCE_SLUG_PREFIX)).filter((c) =>
|
|
5637
|
+
return registryConfigs.filter((c) => c.slug.startsWith(GLOBAL_SOURCE_SLUG_PREFIX)).filter((c) => instructionSourceRejection(c) === null).map((c) => c.slug).sort();
|
|
5432
5638
|
}
|
|
5433
5639
|
function accountedGlobalSourceSlugs(manifest) {
|
|
5434
5640
|
return [...manifest.sources, ...manifest.skippedSources].map((source) => source.id).filter((id) => id.startsWith(GLOBAL_SOURCE_SLUG_PREFIX));
|
|
@@ -5441,6 +5647,10 @@ function computeGlobalSourceCoverage(registryConfigs, configuredSlugs) {
|
|
|
5441
5647
|
const unexpected = [...configuredSet].filter((slug) => slug.startsWith(GLOBAL_SOURCE_SLUG_PREFIX) && !expectedSet.has(slug)).sort();
|
|
5442
5648
|
return {
|
|
5443
5649
|
expectedSlugs: expected,
|
|
5650
|
+
excludedSources: registryConfigs.filter((config) => config.slug.startsWith(GLOBAL_SOURCE_SLUG_PREFIX)).flatMap((config) => {
|
|
5651
|
+
const reason = instructionSourceRejection(config);
|
|
5652
|
+
return reason ? [{ slug: config.slug, reason }] : [];
|
|
5653
|
+
}).sort((left, right) => left.slug.localeCompare(right.slug)),
|
|
5444
5654
|
configuredSlugs: [...configuredSet].sort(),
|
|
5445
5655
|
missingSlugs: missing,
|
|
5446
5656
|
unexpectedSlugs: unexpected,
|
|
@@ -5696,6 +5906,176 @@ function stationProfileSource(env = process.env) {
|
|
|
5696
5906
|
};
|
|
5697
5907
|
}
|
|
5698
5908
|
|
|
5909
|
+
// src/lib/session-refresh.ts
|
|
5910
|
+
import { basename } from "path";
|
|
5911
|
+
var nonempty = exports_external.string().min(1).max(4096);
|
|
5912
|
+
var selectorSchema = exports_external.object({
|
|
5913
|
+
schema: exports_external.literal("hasna.instructions.hosted-profile-selector/v1"),
|
|
5914
|
+
authority: nonempty,
|
|
5915
|
+
profileId: nonempty,
|
|
5916
|
+
providerVersion: nonempty,
|
|
5917
|
+
providerVariant: nonempty.optional(),
|
|
5918
|
+
model: nonempty.optional(),
|
|
5919
|
+
path: nonempty.optional(),
|
|
5920
|
+
manual: exports_external.array(nonempty).max(256),
|
|
5921
|
+
codewithNativeImports: exports_external.boolean(),
|
|
5922
|
+
allowEmptySources: exports_external.boolean(),
|
|
5923
|
+
stationProfile: exports_external.boolean(),
|
|
5924
|
+
checkGlobalCoverage: exports_external.boolean(),
|
|
5925
|
+
assetSurface: nonempty.optional(),
|
|
5926
|
+
assetScope: exports_external.enum(["global", "project", "session"]).optional()
|
|
5927
|
+
}).strict();
|
|
5928
|
+
function authority(value) {
|
|
5929
|
+
const url = new URL(value);
|
|
5930
|
+
if (url.username || url.password || url.search || url.hash || !["https:", "http:"].includes(url.protocol)) {
|
|
5931
|
+
throw new Error("SESSION_REFRESH_AUTHORITY_INVALID: expected a credential-free Instructions API authority.");
|
|
5932
|
+
}
|
|
5933
|
+
return url.toString().replace(/\/$/, "");
|
|
5934
|
+
}
|
|
5935
|
+
function normalizeSessionHostedProfileSelector(value) {
|
|
5936
|
+
const parsed = selectorSchema.parse(value);
|
|
5937
|
+
return { ...parsed, authority: authority(parsed.authority) };
|
|
5938
|
+
}
|
|
5939
|
+
function assertHostedProfileBindings(profileId, configs, bindings) {
|
|
5940
|
+
const configIds = new Set(configs.map((config) => config.id));
|
|
5941
|
+
const bindingIds = new Set(bindings.map((binding) => binding.config_id));
|
|
5942
|
+
if (configIds.size !== configs.length || bindingIds.size !== bindings.length || bindings.some((binding) => binding.profile_id !== profileId || !configIds.has(binding.config_id)) || [...configIds].some((id) => !bindingIds.has(id))) {
|
|
5943
|
+
throw new Error("HOSTED_PROFILE_BINDINGS_INCOMPLETE: exact one-to-one config membership and explicit profile bindings are required; refusing legacy activation fallback.");
|
|
5944
|
+
}
|
|
5945
|
+
for (const row of bindings) {
|
|
5946
|
+
const raw = row.binding;
|
|
5947
|
+
const invalid = () => new Error("HOSTED_PROFILE_BINDINGS_INVALID: each hosted binding must contain a complete explicit schema, activation, required flag and fallback; refusing legacy activation defaults.");
|
|
5948
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
5949
|
+
throw invalid();
|
|
5950
|
+
const record = raw;
|
|
5951
|
+
if (!["schema", "activation", "required", "fallback"].every((key) => Object.hasOwn(record, key)) || typeof record["required"] !== "boolean")
|
|
5952
|
+
throw invalid();
|
|
5953
|
+
try {
|
|
5954
|
+
normalizeProfileConfigBinding(record);
|
|
5955
|
+
} catch {
|
|
5956
|
+
throw invalid();
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
function readManagedManifest(targetHome) {
|
|
5961
|
+
const path = join7(targetHome, SESSION_RENDER_MANIFEST_RELATIVE_PATH);
|
|
5962
|
+
let ancestor = dirname4(path);
|
|
5963
|
+
while (true) {
|
|
5964
|
+
const stat = lstatSync3(ancestor);
|
|
5965
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
5966
|
+
throw new Error("SESSION_REFRESH_PATH_INVALID: managed target uses a symlink or non-directory ancestor.");
|
|
5967
|
+
const parent = dirname4(ancestor);
|
|
5968
|
+
if (parent === ancestor)
|
|
5969
|
+
break;
|
|
5970
|
+
ancestor = parent;
|
|
5971
|
+
}
|
|
5972
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
5973
|
+
let raw;
|
|
5974
|
+
try {
|
|
5975
|
+
const stat = fstatSync(fd);
|
|
5976
|
+
if (!stat.isFile() || stat.size > SESSION_MANAGED_INPUT_MAX_BYTES)
|
|
5977
|
+
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: expected a bounded regular manifest file.");
|
|
5978
|
+
raw = readFileSync3(fd);
|
|
5979
|
+
if (raw.byteLength > SESSION_MANAGED_INPUT_MAX_BYTES)
|
|
5980
|
+
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: manifest grew beyond the read limit.");
|
|
5981
|
+
} finally {
|
|
5982
|
+
closeSync(fd);
|
|
5983
|
+
}
|
|
5984
|
+
const manifest = JSON.parse(raw.toString("utf8"));
|
|
5985
|
+
if (manifest.schema !== SESSION_RENDER_SCHEMA || !SESSION_RENDER_TOOLS.includes(manifest.tool) || manifest.targetHome !== targetHome || !manifest.profile?.trim() || !["session-home", "project-root"].includes(manifest.targetKind) || !Array.isArray(manifest.files) || !manifest.refreshSelector || manifest.targetOwner?.writer?.id !== SESSION_RENDERER_OWNER_ID || manifest.targetOwner?.targetHome !== targetHome) {
|
|
5986
|
+
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: target is not an owned hosted profile render; apply a hosted --compile-profile first.");
|
|
5987
|
+
}
|
|
5988
|
+
manifest.refreshSelector = normalizeSessionHostedProfileSelector(manifest.refreshSelector);
|
|
5989
|
+
return { manifest, sha256: createHash4("sha256").update(raw).digest("hex") };
|
|
5990
|
+
}
|
|
5991
|
+
async function refreshSessionRender(input) {
|
|
5992
|
+
const { store } = input;
|
|
5993
|
+
if (store.mode !== "api" || !store.v1BaseUrl)
|
|
5994
|
+
throw new Error("SESSION_REFRESH_HOSTED_REQUIRED: refresh requires the configured hosted Instructions API.");
|
|
5995
|
+
const targetHome = resolve5(input.targetHome);
|
|
5996
|
+
if (targetHome === parse2(targetHome).root)
|
|
5997
|
+
throw new Error("SESSION_REFRESH_PATH_INVALID: filesystem root is not a managed target.");
|
|
5998
|
+
const previous = readManagedManifest(targetHome);
|
|
5999
|
+
const selector = previous.manifest.refreshSelector;
|
|
6000
|
+
if (authority(store.v1BaseUrl) !== selector.authority)
|
|
6001
|
+
throw new Error("SESSION_REFRESH_AUTHORITY_MISMATCH: configured API differs from the recorded profile authority.");
|
|
6002
|
+
const profile = await store.getProfile(selector.profileId);
|
|
6003
|
+
if (profile.id !== selector.profileId)
|
|
6004
|
+
throw new Error("SESSION_REFRESH_PROFILE_MISMATCH: hosted profile identity changed.");
|
|
6005
|
+
const [configs, bindings, assetBindings] = await Promise.all([
|
|
6006
|
+
store.getProfileConfigs(profile.id),
|
|
6007
|
+
store.getProfileConfigBindings(profile.id, { requireExplicit: true }),
|
|
6008
|
+
store.getProfileAssetBindings(profile.id, { requireExplicit: true })
|
|
6009
|
+
]);
|
|
6010
|
+
assertHostedProfileBindings(profile.id, configs, bindings);
|
|
6011
|
+
if (configs.length > 2048 || bindings.length > 8192 || assetBindings.length > 1024)
|
|
6012
|
+
throw new Error("SESSION_REFRESH_INPUT_LIMIT: hosted profile exceeds refresh bounds.");
|
|
6013
|
+
const totalBytes = configs.reduce((sum, config) => sum + Buffer.byteLength(config.content), 0);
|
|
6014
|
+
if (totalBytes > SESSION_MANAGED_INPUT_MAX_BYTES)
|
|
6015
|
+
throw new Error("SESSION_REFRESH_INPUT_LIMIT: hosted instruction payload exceeds refresh byte bounds.");
|
|
6016
|
+
const assetConfigs = await Promise.all([...new Set(assetBindings.map((binding) => binding.source_config_id))].map((id) => store.getConfigById(id)));
|
|
6017
|
+
if (assetConfigs.reduce((sum, config) => sum + Buffer.byteLength(config.content), totalBytes) > SESSION_MANAGED_INPUT_MAX_BYTES)
|
|
6018
|
+
throw new Error("SESSION_REFRESH_INPUT_LIMIT: hosted source and asset payload exceeds refresh byte bounds.");
|
|
6019
|
+
let ownedClaudeAuthorities;
|
|
6020
|
+
if (previous.manifest.tool === "claude") {
|
|
6021
|
+
ownedClaudeAuthorities = (await store.listConfigs({ category: "rules", agent: "claude", kind: "file" })).filter((config) => config.target_path && basename(normalizeTargetPath(config.target_path)) === "AGENTS.md").map((config) => ({ slug: config.slug, targetPath: config.target_path, content: config.content }));
|
|
6022
|
+
}
|
|
6023
|
+
const station = selector.stationProfile ? stationProfileSource() : null;
|
|
6024
|
+
const plan = planProfileSessionRender({
|
|
6025
|
+
tool: previous.manifest.tool,
|
|
6026
|
+
profile: previous.manifest.profile,
|
|
6027
|
+
profile_id: profile.id,
|
|
6028
|
+
provider_version: selector.providerVersion,
|
|
6029
|
+
targetHome,
|
|
6030
|
+
...previous.manifest.targetKind === "project-root" ? { projectRoot: targetHome } : {},
|
|
6031
|
+
sessionId: previous.manifest.sessionId ?? undefined,
|
|
6032
|
+
refreshSelector: selector,
|
|
6033
|
+
codewithNativeImports: selector.codewithNativeImports,
|
|
6034
|
+
allowEmptySources: selector.allowEmptySources,
|
|
6035
|
+
configs,
|
|
6036
|
+
bindings,
|
|
6037
|
+
asset_configs: assetConfigs,
|
|
6038
|
+
asset_bindings: assetBindings,
|
|
6039
|
+
asset_plan_mode: input.dryRun ? "dry-run" : "apply",
|
|
6040
|
+
asset_scope: selector.assetScope,
|
|
6041
|
+
asset_surface: selector.assetSurface,
|
|
6042
|
+
extra_sources: station ? [station] : undefined,
|
|
6043
|
+
ownedClaudeAuthorities,
|
|
6044
|
+
graph_context: {
|
|
6045
|
+
...selector.providerVariant ? { provider_variant: selector.providerVariant } : {},
|
|
6046
|
+
...selector.model ? { model: selector.model } : {},
|
|
6047
|
+
...selector.path ? { path: selector.path } : {},
|
|
6048
|
+
...selector.manual.length ? { manual: selector.manual } : {}
|
|
6049
|
+
}
|
|
6050
|
+
});
|
|
6051
|
+
if (selector.checkGlobalCoverage) {
|
|
6052
|
+
const coverage = computeGlobalSourceCoverage(await store.listConfigs({}), accountedGlobalSourceSlugs(plan.manifest));
|
|
6053
|
+
if (!coverage.complete)
|
|
6054
|
+
throw new Error(`SESSION_REFRESH_COVERAGE_INCOMPLETE: ${coverage.missingSlugs.length} registered global sources are absent.`);
|
|
6055
|
+
}
|
|
6056
|
+
const preview = applySessionRender(plan, { dryRun: true, expectedManifestSha256: previous.sha256, ownedClaudeAuthorities });
|
|
6057
|
+
const common = { checkedAt: new Date().toISOString(), targetHome, selector, sourceHash: plan.manifest.sourceHash };
|
|
6058
|
+
if (preview.conflicts.length)
|
|
6059
|
+
return { ...common, status: "blocked", apply: preview };
|
|
6060
|
+
if (!preview.drift.clean)
|
|
6061
|
+
throw new Error("SESSION_REFRESH_DRIFT: generated files changed or disappeared since their last managed render.");
|
|
6062
|
+
const sameSources = previous.manifest.sourceHash === plan.manifest.sourceHash;
|
|
6063
|
+
const samePayloads = preview.files.filter((file) => file.role !== "manifest").every((file) => file.action === "unchanged");
|
|
6064
|
+
if (sameSources && samePayloads) {
|
|
6065
|
+
if (readManagedManifest(targetHome).sha256 !== previous.sha256 || !checkSessionRenderDrift(targetHome).clean)
|
|
6066
|
+
throw new Error("SESSION_REFRESH_DRIFT: managed target changed during refresh.");
|
|
6067
|
+
return { ...common, status: "unchanged", apply: { ...preview, files: preview.files.map((file) => file.role === "manifest" ? { ...file, action: "unchanged", changed: false, newSha256: previous.sha256, reason: "hosted sources and generated payloads unchanged" } : file) } };
|
|
6068
|
+
}
|
|
6069
|
+
if (input.dryRun)
|
|
6070
|
+
return { ...common, status: "dry-run", apply: preview };
|
|
6071
|
+
const applied = applySessionRender(plan, { expectedManifestSha256: previous.sha256, ownedClaudeAuthorities });
|
|
6072
|
+
if (applied.conflicts.length)
|
|
6073
|
+
return { ...common, status: "blocked", apply: applied };
|
|
6074
|
+
if (!applied.applied || !checkSessionRenderDrift(targetHome).clean)
|
|
6075
|
+
throw new Error("SESSION_REFRESH_READBACK_FAILED: applied instructions did not verify against their manifest.");
|
|
6076
|
+
return { ...common, status: "updated", apply: applied };
|
|
6077
|
+
}
|
|
6078
|
+
|
|
5699
6079
|
// src/lib/project-dashboard-standard.ts
|
|
5700
6080
|
var PROJECT_DASHBOARD_STANDARD_SLUG = "agent-managed-project-dashboard-standard";
|
|
5701
6081
|
var PROJECT_DASHBOARD_PROFILE_VARIABLES = {
|
|
@@ -5788,11 +6168,15 @@ async function ensureProjectDashboardStandardConfig(store = resolveConfigStore()
|
|
|
5788
6168
|
};
|
|
5789
6169
|
try {
|
|
5790
6170
|
const existing = await store.getConfig(PROJECT_DASHBOARD_STANDARD_SLUG);
|
|
6171
|
+
if (isRetiredInstructionSource(existing))
|
|
6172
|
+
return existing;
|
|
5791
6173
|
if (existing.content !== input.content || existing.description !== input.description || existing.category !== input.category || existing.agent !== input.agent || existing.format !== input.format || existing.kind !== input.kind) {
|
|
5792
6174
|
return await store.updateConfig(existing.id, input);
|
|
5793
6175
|
}
|
|
5794
6176
|
return existing;
|
|
5795
|
-
} catch {
|
|
6177
|
+
} catch (error) {
|
|
6178
|
+
if (!(error instanceof ConfigNotFoundError))
|
|
6179
|
+
throw error;
|
|
5796
6180
|
return await store.createConfig(input);
|
|
5797
6181
|
}
|
|
5798
6182
|
}
|
|
@@ -5805,10 +6189,9 @@ function profileHasSelectors(profile) {
|
|
|
5805
6189
|
var PLATFORM_PROFILE_PRESETS = [
|
|
5806
6190
|
{
|
|
5807
6191
|
name: "linux-arm64",
|
|
5808
|
-
description: "Default
|
|
5809
|
-
selectors: { os: ["linux"], arch: ["arm64"]
|
|
6192
|
+
description: "Default reviewed instruction profile for Linux arm64",
|
|
6193
|
+
selectors: { os: ["linux"], arch: ["arm64"] },
|
|
5810
6194
|
variables: {
|
|
5811
|
-
WORKSPACE_ROOT: "{{HOME_DIR}}/workspace",
|
|
5812
6195
|
BUN_BIN_DIR: "{{HOME_DIR}}/.bun/bin",
|
|
5813
6196
|
BUN_PATH: "{{BUN_BIN_DIR}}/bun",
|
|
5814
6197
|
PATH_PREFIX: "{{BUN_BIN_DIR}}",
|
|
@@ -5817,10 +6200,9 @@ var PLATFORM_PROFILE_PRESETS = [
|
|
|
5817
6200
|
},
|
|
5818
6201
|
{
|
|
5819
6202
|
name: "macos-arm64",
|
|
5820
|
-
description: "Default
|
|
5821
|
-
selectors: { os: ["macos"], arch: ["arm64"]
|
|
6203
|
+
description: "Default reviewed instruction profile for macOS arm64",
|
|
6204
|
+
selectors: { os: ["macos"], arch: ["arm64"] },
|
|
5822
6205
|
variables: {
|
|
5823
|
-
WORKSPACE_ROOT: "{{HOME_DIR}}/Workspace",
|
|
5824
6206
|
BUN_BIN_DIR: "{{HOME_DIR}}/.bun/bin",
|
|
5825
6207
|
BUN_PATH: "/opt/homebrew/bin/bun",
|
|
5826
6208
|
PATH_PREFIX: "/opt/homebrew/bin:{{BUN_BIN_DIR}}",
|
|
@@ -5829,12 +6211,21 @@ var PLATFORM_PROFILE_PRESETS = [
|
|
|
5829
6211
|
}
|
|
5830
6212
|
];
|
|
5831
6213
|
async function ensurePlatformProfiles(store = resolveConfigStore()) {
|
|
5832
|
-
const configs = await store.listConfigs();
|
|
5833
6214
|
const ensured = [];
|
|
5834
|
-
|
|
5835
|
-
let profile;
|
|
6215
|
+
const existingProfiles = await Promise.all(PLATFORM_PROFILE_PRESETS.map(async (preset) => {
|
|
5836
6216
|
try {
|
|
5837
|
-
|
|
6217
|
+
return await store.getProfile(preset.name);
|
|
6218
|
+
} catch (error) {
|
|
6219
|
+
if (error instanceof ProfileNotFoundError)
|
|
6220
|
+
return null;
|
|
6221
|
+
throw error;
|
|
6222
|
+
}
|
|
6223
|
+
}));
|
|
6224
|
+
for (const [index, preset] of PLATFORM_PROFILE_PRESETS.entries()) {
|
|
6225
|
+
let profile;
|
|
6226
|
+
const existing = existingProfiles[index];
|
|
6227
|
+
if (existing) {
|
|
6228
|
+
profile = existing;
|
|
5838
6229
|
const selectors = mergeProfileSelectors(preset.selectors, profile.selectors);
|
|
5839
6230
|
const variables = mergeProfileVariables(preset.variables, profile.variables);
|
|
5840
6231
|
if (JSON.stringify(selectors) !== JSON.stringify(profile.selectors) || JSON.stringify(variables) !== JSON.stringify(profile.variables)) {
|
|
@@ -5844,12 +6235,9 @@ async function ensurePlatformProfiles(store = resolveConfigStore()) {
|
|
|
5844
6235
|
variables
|
|
5845
6236
|
});
|
|
5846
6237
|
}
|
|
5847
|
-
}
|
|
6238
|
+
} else {
|
|
5848
6239
|
profile = await store.createProfile(preset);
|
|
5849
6240
|
}
|
|
5850
|
-
for (const config of configs) {
|
|
5851
|
-
await store.addConfigToProfile(profile.id, config.id);
|
|
5852
|
-
}
|
|
5853
6241
|
ensured.push(profile);
|
|
5854
6242
|
}
|
|
5855
6243
|
return ensured;
|
|
@@ -5990,29 +6378,33 @@ async function ensureDangerousOperationGuardStandardConfig(store = resolveConfig
|
|
|
5990
6378
|
};
|
|
5991
6379
|
try {
|
|
5992
6380
|
const existing = await store.getConfig(DANGEROUS_OPERATION_GUARD_STANDARD_SLUG);
|
|
6381
|
+
if (isRetiredInstructionSource(existing))
|
|
6382
|
+
return existing;
|
|
5993
6383
|
if (existing.content !== input.content || existing.description !== input.description || existing.category !== input.category || existing.agent !== input.agent || existing.format !== input.format || existing.kind !== input.kind || JSON.stringify(existing.tags) !== JSON.stringify(input.tags)) {
|
|
5994
6384
|
return await store.updateConfig(existing.id, input);
|
|
5995
6385
|
}
|
|
5996
6386
|
return existing;
|
|
5997
|
-
} catch {
|
|
6387
|
+
} catch (error) {
|
|
6388
|
+
if (!(error instanceof ConfigNotFoundError))
|
|
6389
|
+
throw error;
|
|
5998
6390
|
return await store.createConfig(input);
|
|
5999
6391
|
}
|
|
6000
6392
|
}
|
|
6001
6393
|
|
|
6002
6394
|
// src/lib/managed-skill-runtimes.ts
|
|
6003
|
-
import { createHash as
|
|
6395
|
+
import { createHash as createHash5 } from "crypto";
|
|
6004
6396
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6005
6397
|
import {
|
|
6006
6398
|
existsSync as existsSync7,
|
|
6007
|
-
lstatSync as
|
|
6399
|
+
lstatSync as lstatSync4,
|
|
6008
6400
|
mkdirSync as mkdirSync4,
|
|
6009
|
-
readFileSync as
|
|
6401
|
+
readFileSync as readFileSync4,
|
|
6010
6402
|
renameSync,
|
|
6011
6403
|
rmSync as rmSync2,
|
|
6012
6404
|
writeFileSync as writeFileSync3
|
|
6013
6405
|
} from "fs";
|
|
6014
6406
|
import { homedir as homedir3 } from "os";
|
|
6015
|
-
import { dirname as
|
|
6407
|
+
import { dirname as dirname5, join as join8, parse as parse3, relative as relative2, resolve as resolve6 } from "path";
|
|
6016
6408
|
var INBOX_CONVERSATIONS_MINIMUM_VERSION = "0.5.28";
|
|
6017
6409
|
var INBOX_SKILL_MARKERS = [
|
|
6018
6410
|
[".claude", "skills", "inbox", "SKILL.md"],
|
|
@@ -6023,25 +6415,25 @@ var INBOX_SKILL_MARKERS = [
|
|
|
6023
6415
|
];
|
|
6024
6416
|
var REQUIRED_WATCH_FLAGS = ["--from <agent>", "--all", "--full-content"];
|
|
6025
6417
|
function sha2564(content) {
|
|
6026
|
-
return
|
|
6418
|
+
return createHash5("sha256").update(content).digest("hex");
|
|
6027
6419
|
}
|
|
6028
6420
|
function lstatOrNull(path) {
|
|
6029
6421
|
try {
|
|
6030
|
-
return
|
|
6422
|
+
return lstatSync4(path);
|
|
6031
6423
|
} catch {
|
|
6032
6424
|
return null;
|
|
6033
6425
|
}
|
|
6034
6426
|
}
|
|
6035
6427
|
function findSymlinkedAncestor(path) {
|
|
6036
|
-
const normalized =
|
|
6037
|
-
const parsed =
|
|
6428
|
+
const normalized = resolve6(path);
|
|
6429
|
+
const parsed = parse3(normalized);
|
|
6038
6430
|
let current = parsed.root;
|
|
6039
6431
|
const rel = relative2(parsed.root, normalized);
|
|
6040
6432
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
6041
|
-
current =
|
|
6433
|
+
current = join8(current, segment);
|
|
6042
6434
|
if (!existsSync7(current))
|
|
6043
6435
|
return null;
|
|
6044
|
-
if (
|
|
6436
|
+
if (lstatSync4(current).isSymbolicLink())
|
|
6045
6437
|
return current;
|
|
6046
6438
|
}
|
|
6047
6439
|
return null;
|
|
@@ -6063,7 +6455,7 @@ function readCanonicalSkill(explicitPath) {
|
|
|
6063
6455
|
if (!stat?.isFile()) {
|
|
6064
6456
|
throw new Error("packaged inbox skill contract is not a regular file");
|
|
6065
6457
|
}
|
|
6066
|
-
const content =
|
|
6458
|
+
const content = readFileSync4(assetPath, "utf8");
|
|
6067
6459
|
if (!content.includes("conversations watch --from <agent> --all")) {
|
|
6068
6460
|
throw new Error("packaged inbox skill contract does not declare the canonical conversations watcher");
|
|
6069
6461
|
}
|
|
@@ -6101,7 +6493,7 @@ function compareVersions(left, right) {
|
|
|
6101
6493
|
return 0;
|
|
6102
6494
|
}
|
|
6103
6495
|
function inspectSkillMarkers(homeDir2) {
|
|
6104
|
-
return INBOX_SKILL_MARKERS.map((parts) =>
|
|
6496
|
+
return INBOX_SKILL_MARKERS.map((parts) => join8(homeDir2, ...parts)).map((path) => {
|
|
6105
6497
|
const stat = lstatOrNull(path);
|
|
6106
6498
|
if (!stat)
|
|
6107
6499
|
return null;
|
|
@@ -6110,7 +6502,7 @@ function inspectSkillMarkers(homeDir2) {
|
|
|
6110
6502
|
}
|
|
6111
6503
|
return {
|
|
6112
6504
|
path,
|
|
6113
|
-
content:
|
|
6505
|
+
content: readFileSync4(path, "utf8"),
|
|
6114
6506
|
mode: stat.mode & 511,
|
|
6115
6507
|
regular: true
|
|
6116
6508
|
};
|
|
@@ -6145,7 +6537,7 @@ function inspectInbox(options) {
|
|
|
6145
6537
|
let reason = "skill not installed";
|
|
6146
6538
|
if (skillPresent) {
|
|
6147
6539
|
const nonRegular = snapshots.some((snapshot) => !snapshot.regular);
|
|
6148
|
-
const symlinkAncestor = snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(
|
|
6540
|
+
const symlinkAncestor = snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname5(path))).find((found) => found !== null);
|
|
6149
6541
|
if (nonRegular)
|
|
6150
6542
|
reason = "managed skill target is not a regular file";
|
|
6151
6543
|
else if (symlinkAncestor)
|
|
@@ -6232,10 +6624,10 @@ function cleanup(path) {
|
|
|
6232
6624
|
rmSync2(path, { force: true });
|
|
6233
6625
|
}
|
|
6234
6626
|
function writeAtomic(path, content, mode) {
|
|
6235
|
-
assertNoSymlinkAncestors2(
|
|
6627
|
+
assertNoSymlinkAncestors2(dirname5(path));
|
|
6236
6628
|
const tempPath = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
6237
6629
|
try {
|
|
6238
|
-
mkdirSync4(
|
|
6630
|
+
mkdirSync4(dirname5(path), { recursive: true, mode: 493 });
|
|
6239
6631
|
writeFileSync3(tempPath, content, { mode, flag: "wx" });
|
|
6240
6632
|
renameSync(tempPath, path);
|
|
6241
6633
|
} finally {
|
|
@@ -6244,7 +6636,7 @@ function writeAtomic(path, content, mode) {
|
|
|
6244
6636
|
}
|
|
6245
6637
|
var DEFAULT_SKILL_WRITE_FILE_OPERATIONS = {
|
|
6246
6638
|
lstat: lstatOrNull,
|
|
6247
|
-
read: (path) =>
|
|
6639
|
+
read: (path) => readFileSync4(path, "utf8"),
|
|
6248
6640
|
write: writeAtomic
|
|
6249
6641
|
};
|
|
6250
6642
|
function writeSkillContractsTransactional(snapshots, canonicalContent, fileOperations = DEFAULT_SKILL_WRITE_FILE_OPERATIONS) {
|
|
@@ -6311,7 +6703,7 @@ async function reconcileManagedSkillRuntimes(options = {}) {
|
|
|
6311
6703
|
dry_run: dryRun
|
|
6312
6704
|
};
|
|
6313
6705
|
}
|
|
6314
|
-
const symlinkedAncestor = before.snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(
|
|
6706
|
+
const symlinkedAncestor = before.snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname5(path))).find((found) => found !== null);
|
|
6315
6707
|
if (symlinkedAncestor) {
|
|
6316
6708
|
return {
|
|
6317
6709
|
runtimes: [{ ...status, action: "failed", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
@@ -6388,28 +6780,28 @@ async function reconcileManagedSkillRuntimes(options = {}) {
|
|
|
6388
6780
|
}
|
|
6389
6781
|
|
|
6390
6782
|
// src/status.ts
|
|
6391
|
-
import { existsSync as existsSync9, readFileSync as
|
|
6783
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
6392
6784
|
|
|
6393
6785
|
// src/lib/package-version.ts
|
|
6394
|
-
import { existsSync as existsSync8, readFileSync as
|
|
6395
|
-
import { dirname as
|
|
6786
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
6787
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
6396
6788
|
import { fileURLToPath } from "url";
|
|
6397
6789
|
var cached = null;
|
|
6398
6790
|
function getPackageVersion() {
|
|
6399
6791
|
if (cached)
|
|
6400
6792
|
return cached;
|
|
6401
6793
|
try {
|
|
6402
|
-
let dir =
|
|
6794
|
+
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
6403
6795
|
for (let i = 0;i < 8; i++) {
|
|
6404
|
-
const pkgPath =
|
|
6796
|
+
const pkgPath = join9(dir, "package.json");
|
|
6405
6797
|
if (existsSync8(pkgPath)) {
|
|
6406
|
-
const pkg = JSON.parse(
|
|
6798
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
6407
6799
|
if (pkg.name === "@hasna/instructions" && pkg.version) {
|
|
6408
6800
|
cached = pkg.version;
|
|
6409
6801
|
return cached;
|
|
6410
6802
|
}
|
|
6411
6803
|
}
|
|
6412
|
-
const parent =
|
|
6804
|
+
const parent = dirname6(dir);
|
|
6413
6805
|
if (parent === dir)
|
|
6414
6806
|
break;
|
|
6415
6807
|
dir = parent;
|
|
@@ -6482,7 +6874,7 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
|
|
|
6482
6874
|
missingTargets += 1;
|
|
6483
6875
|
continue;
|
|
6484
6876
|
}
|
|
6485
|
-
const disk =
|
|
6877
|
+
const disk = readFileSync6(targetPath, "utf-8");
|
|
6486
6878
|
const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(config.target_path, config.format));
|
|
6487
6879
|
if (redactedDisk !== config.content) {
|
|
6488
6880
|
driftedTargets += 1;
|
|
@@ -6574,9 +6966,9 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
|
|
|
6574
6966
|
}
|
|
6575
6967
|
|
|
6576
6968
|
// src/lib/provider-context.ts
|
|
6577
|
-
import { createHash as
|
|
6578
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as
|
|
6579
|
-
import { join as
|
|
6969
|
+
import { createHash as createHash6 } from "crypto";
|
|
6970
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
6971
|
+
import { join as join10 } from "path";
|
|
6580
6972
|
var PROVIDER_CONTEXT_DIR = ".hasna/provider-context";
|
|
6581
6973
|
var PROVIDER_CONTEXT_MANIFEST = "manifest.json";
|
|
6582
6974
|
var PROVIDER_CONTEXT_SCHEMA = "hasna.instructions.provider-context/v1";
|
|
@@ -6712,7 +7104,7 @@ function renderProviderFragment(entry) {
|
|
|
6712
7104
|
return entry ? renderPerEndpointFragment(entry) : INVARIANT_FRAGMENT;
|
|
6713
7105
|
}
|
|
6714
7106
|
function sha2565(content) {
|
|
6715
|
-
return
|
|
7107
|
+
return createHash6("sha256").update(content).digest("hex");
|
|
6716
7108
|
}
|
|
6717
7109
|
function resolveAndRenderProviderContext(opts) {
|
|
6718
7110
|
const entry = matchProviderEndpoint(opts.origin);
|
|
@@ -6721,18 +7113,18 @@ function resolveAndRenderProviderContext(opts) {
|
|
|
6721
7113
|
const recordedEndpoint = originAccepted ? `${opts.origin.host}${opts.origin.pathPrefix || ""}` : null;
|
|
6722
7114
|
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;
|
|
6723
7115
|
const content = renderProviderFragment(entry);
|
|
6724
|
-
const dir =
|
|
7116
|
+
const dir = join10(opts.homeDir, PROVIDER_CONTEXT_DIR);
|
|
6725
7117
|
if (!existsSync10(dir))
|
|
6726
7118
|
mkdirSync5(dir, { recursive: true });
|
|
6727
7119
|
const filename = `${entry ? entry.key : "invariant"}.md`;
|
|
6728
|
-
const fragmentPath =
|
|
7120
|
+
const fragmentPath = join10(dir, filename);
|
|
6729
7121
|
const fragmentSha256 = sha2565(content);
|
|
6730
7122
|
writeFileSync4(fragmentPath, content, "utf8");
|
|
6731
|
-
const manifestPath =
|
|
7123
|
+
const manifestPath = join10(dir, PROVIDER_CONTEXT_MANIFEST);
|
|
6732
7124
|
let manifest = { schema: PROVIDER_CONTEXT_SCHEMA, fragments: {} };
|
|
6733
7125
|
try {
|
|
6734
7126
|
if (existsSync10(manifestPath)) {
|
|
6735
|
-
const parsed = JSON.parse(
|
|
7127
|
+
const parsed = JSON.parse(readFileSync7(manifestPath, "utf8"));
|
|
6736
7128
|
if (parsed && typeof parsed === "object")
|
|
6737
7129
|
manifest = parsed;
|
|
6738
7130
|
}
|
|
@@ -6798,6 +7190,9 @@ function printLine(text = "") {
|
|
|
6798
7190
|
function printJson(value) {
|
|
6799
7191
|
printLine(JSON.stringify(value, null, 2));
|
|
6800
7192
|
}
|
|
7193
|
+
function printMachineJson(value, pretty = false) {
|
|
7194
|
+
printLine(JSON.stringify(value, null, pretty ? 2 : undefined));
|
|
7195
|
+
}
|
|
6801
7196
|
function printManagedSkillRuntimeReport(report) {
|
|
6802
7197
|
for (const runtime of report.runtimes) {
|
|
6803
7198
|
if (!runtime.skill_present)
|
|
@@ -6819,7 +7214,7 @@ function fmtConfig(c, format) {
|
|
|
6819
7214
|
` ${chalk.cyan("category:")} ${c.category} ${chalk.cyan("agent:")} ${c.agent} ${chalk.cyan("kind:")} ${c.kind}`,
|
|
6820
7215
|
` ${chalk.cyan("format:")} ${c.format} ${chalk.cyan("version:")} ${c.version}${c.target_path ? ` ${chalk.cyan("path:")} ${c.target_path}` : ""}`,
|
|
6821
7216
|
c.description ? ` ${chalk.dim(c.description)}` : "",
|
|
6822
|
-
c.tags
|
|
7217
|
+
c.tags?.length ? ` ${chalk.dim("tags: " + c.tags.join(", "))}` : ""
|
|
6823
7218
|
].filter(Boolean).join(`
|
|
6824
7219
|
`);
|
|
6825
7220
|
}
|
|
@@ -6837,7 +7232,7 @@ function printConfigRows(configs) {
|
|
|
6837
7232
|
for (const c of configs) {
|
|
6838
7233
|
const type = `${c.category}/${c.agent}`;
|
|
6839
7234
|
const path = c.kind === "reference" ? "(ref)" : c.target_path ?? "(no path)";
|
|
6840
|
-
console.log(`${pad(c.slug, 32)} ${pad(type, 15)} ${pad(c.format, 8)} ${pad(truncateMiddle(path, 44), 44)} ${String(c.
|
|
7235
|
+
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}`);
|
|
6841
7236
|
}
|
|
6842
7237
|
}
|
|
6843
7238
|
function splitCsv(value) {
|
|
@@ -6879,7 +7274,7 @@ function parseSessionSource(value, order) {
|
|
|
6879
7274
|
throw new Error(`Instruction source file not found: ${absPath}`);
|
|
6880
7275
|
const content = readSessionInstructionSourceFile(absPath);
|
|
6881
7276
|
const source = sourceFromFilePath(absPath, content, order);
|
|
6882
|
-
const resolvedId = id || source.id ||
|
|
7277
|
+
const resolvedId = id || source.id || basename2(absPath);
|
|
6883
7278
|
return {
|
|
6884
7279
|
...source,
|
|
6885
7280
|
id: resolvedId,
|
|
@@ -6919,7 +7314,7 @@ function sessionSourceReplacements(values) {
|
|
|
6919
7314
|
return replacements;
|
|
6920
7315
|
}
|
|
6921
7316
|
function readSessionInstructionSourceFile(path) {
|
|
6922
|
-
const stat =
|
|
7317
|
+
const stat = lstatSync5(path);
|
|
6923
7318
|
if (stat.isSymbolicLink()) {
|
|
6924
7319
|
throw new Error("SESSION_SOURCE_SYMLINK_REJECTED: instruction source file must be a regular non-symlink file");
|
|
6925
7320
|
}
|
|
@@ -6929,7 +7324,7 @@ function readSessionInstructionSourceFile(path) {
|
|
|
6929
7324
|
if (stat.size > SESSION_MANAGED_INPUT_MAX_BYTES) {
|
|
6930
7325
|
throw new Error(`SESSION_SOURCE_INPUT_TOO_LARGE: instruction source file exceeds ${SESSION_MANAGED_INPUT_MAX_BYTES} bytes`);
|
|
6931
7326
|
}
|
|
6932
|
-
return
|
|
7327
|
+
return readFileSync8(path, "utf-8");
|
|
6933
7328
|
}
|
|
6934
7329
|
function parseLayeredReference(value) {
|
|
6935
7330
|
const trimmed = value.trim();
|
|
@@ -6952,13 +7347,15 @@ async function collectSessionSources(opts, tool, store) {
|
|
|
6952
7347
|
const sources = (opts.source ?? []).map((value, index) => parseSessionSource(value, index));
|
|
6953
7348
|
for (const value of opts.config ?? []) {
|
|
6954
7349
|
const { layer, id } = parseLayeredReference(value);
|
|
6955
|
-
|
|
7350
|
+
const config = await store.getConfig(id);
|
|
7351
|
+
assertInstructionSource(config);
|
|
7352
|
+
sources.push(sourceFromConfig(config, sources.length, layer));
|
|
6956
7353
|
}
|
|
6957
7354
|
for (const value of opts.identityExport ?? []) {
|
|
6958
7355
|
const path = resolveSessionPath(value);
|
|
6959
7356
|
if (!existsSync11(path))
|
|
6960
7357
|
throw new Error(`Identity instruction export not found: ${path}`);
|
|
6961
|
-
const parsed = JSON.parse(
|
|
7358
|
+
const parsed = JSON.parse(readFileSync8(path, "utf-8"));
|
|
6962
7359
|
sources.push(...sourcesFromIdentityExport(parsed, { path, tool, orderOffset: sources.length }));
|
|
6963
7360
|
}
|
|
6964
7361
|
return sources.map((source) => {
|
|
@@ -6974,7 +7371,7 @@ async function collectSessionSources(opts, tool, store) {
|
|
|
6974
7371
|
}
|
|
6975
7372
|
async function loadOwnedClaudeAuthorities(store) {
|
|
6976
7373
|
const configs = await store.listConfigs({ category: "rules", agent: "claude", kind: "file" });
|
|
6977
|
-
return configs.filter((config) => config.target_path &&
|
|
7374
|
+
return configs.filter((config) => config.target_path && basename2(normalizeTargetPath(config.target_path)) === "AGENTS.md").map((config) => ({ slug: config.slug, targetPath: config.target_path, content: config.content }));
|
|
6978
7375
|
}
|
|
6979
7376
|
async function buildSessionRenderPlan(opts, tool, store, assetPlanMode = "dry-run") {
|
|
6980
7377
|
const station = opts.stationProfile === false ? null : stationProfileSource();
|
|
@@ -7004,8 +7401,10 @@ async function buildSessionRenderPlan(opts, tool, store, assetPlanMode = "dry-ru
|
|
|
7004
7401
|
}
|
|
7005
7402
|
const profile = await store.getProfile(opts.compileProfile);
|
|
7006
7403
|
const configs = await store.getProfileConfigs(profile.id);
|
|
7007
|
-
const bindings = await store.getProfileConfigBindings(profile.id);
|
|
7008
|
-
|
|
7404
|
+
const bindings = await store.getProfileConfigBindings(profile.id, { requireExplicit: store.mode === "api" });
|
|
7405
|
+
if (store.mode === "api")
|
|
7406
|
+
assertHostedProfileBindings(profile.id, configs, bindings);
|
|
7407
|
+
const assetBindings = await store.getProfileAssetBindings(profile.id, { requireExplicit: store.mode === "api" });
|
|
7009
7408
|
const assetConfigs = await Promise.all([...new Set(assetBindings.map((binding) => binding.source_config_id))].map((id) => store.getConfigById(id)));
|
|
7010
7409
|
return planProfileSessionRender({
|
|
7011
7410
|
profile_id: profile.id,
|
|
@@ -7015,6 +7414,24 @@ async function buildSessionRenderPlan(opts, tool, store, assetPlanMode = "dry-ru
|
|
|
7015
7414
|
targetHome: opts.targetHome,
|
|
7016
7415
|
projectRoot: opts.projectRoot,
|
|
7017
7416
|
sessionId: opts.sessionId,
|
|
7417
|
+
...store.mode === "api" && store.v1BaseUrl ? {
|
|
7418
|
+
refreshSelector: normalizeSessionHostedProfileSelector({
|
|
7419
|
+
schema: "hasna.instructions.hosted-profile-selector/v1",
|
|
7420
|
+
authority: store.v1BaseUrl,
|
|
7421
|
+
profileId: profile.id,
|
|
7422
|
+
providerVersion: opts.providerVersion,
|
|
7423
|
+
...opts.providerVariant ? { providerVariant: opts.providerVariant } : {},
|
|
7424
|
+
...opts.model ? { model: opts.model } : {},
|
|
7425
|
+
...opts.path ? { path: opts.path } : {},
|
|
7426
|
+
manual: opts.manual ?? [],
|
|
7427
|
+
codewithNativeImports: opts.codewithNativeImports === true || process.env[CODEWITH_NATIVE_IMPORTS_ENV] === "1" || process.env[CODEWITH_NATIVE_IMPORTS_ENV] === "true",
|
|
7428
|
+
allowEmptySources: opts.allowEmptySources === true,
|
|
7429
|
+
stationProfile: opts.stationProfile !== false,
|
|
7430
|
+
checkGlobalCoverage: opts.checkGlobalCoverage === true,
|
|
7431
|
+
...opts.assetSurface ? { assetSurface: opts.assetSurface } : {},
|
|
7432
|
+
...opts.assetScope ? { assetScope: opts.assetScope } : {}
|
|
7433
|
+
})
|
|
7434
|
+
} : {},
|
|
7018
7435
|
codewithNativeImports: opts.codewithNativeImports,
|
|
7019
7436
|
allowEmptySources: opts.allowEmptySources,
|
|
7020
7437
|
configs,
|
|
@@ -7095,14 +7512,14 @@ function readProjectContextBundleOption(value, allowMissing = false) {
|
|
|
7095
7512
|
return {};
|
|
7096
7513
|
throw new ProjectContextError("PROJECT_CONTEXT_INPUT_MISSING", `bundle file not found: ${path}`);
|
|
7097
7514
|
}
|
|
7098
|
-
const stat =
|
|
7515
|
+
const stat = lstatSync5(path);
|
|
7099
7516
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
7100
7517
|
throw new ProjectContextError("PROJECT_CONTEXT_SYMLINK_REJECTED", "bundle input must be a regular non-symlink file");
|
|
7101
7518
|
}
|
|
7102
7519
|
if (stat.size > PROJECT_CONTEXT_MAX_INPUT_BYTES) {
|
|
7103
7520
|
throw new ProjectContextError("PROJECT_CONTEXT_INPUT_TOO_LARGE", `bundle exceeds ${PROJECT_CONTEXT_MAX_INPUT_BYTES} bytes`);
|
|
7104
7521
|
}
|
|
7105
|
-
return { json:
|
|
7522
|
+
return { json: readFileSync8(path, "utf8"), sourcePath: path };
|
|
7106
7523
|
}
|
|
7107
7524
|
function readBoundedProjectContextStdin() {
|
|
7108
7525
|
const chunks = [];
|
|
@@ -7189,24 +7606,245 @@ async function getMachineProfileContext(opts, store, readOptions = {}) {
|
|
|
7189
7606
|
const profile = resolution.profile;
|
|
7190
7607
|
return { machine, profile, resolution, vars: resolveProfileVariables(profile, machine) };
|
|
7191
7608
|
}
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7609
|
+
var COMPACT_CONFIG_FIELDS = [
|
|
7610
|
+
"id",
|
|
7611
|
+
"slug",
|
|
7612
|
+
"name",
|
|
7613
|
+
"kind",
|
|
7614
|
+
"category",
|
|
7615
|
+
"agent",
|
|
7616
|
+
"format",
|
|
7617
|
+
"is_template",
|
|
7618
|
+
"version",
|
|
7619
|
+
"updated_at"
|
|
7620
|
+
];
|
|
7621
|
+
var OPTIONAL_CONFIG_IDENTITY_FIELDS = ["created_at", "synced_at"];
|
|
7622
|
+
var FULL_CONFIG_FIELDS = [
|
|
7623
|
+
"id",
|
|
7624
|
+
"name",
|
|
7625
|
+
"slug",
|
|
7626
|
+
"kind",
|
|
7627
|
+
"category",
|
|
7628
|
+
"agent",
|
|
7629
|
+
"target_path",
|
|
7630
|
+
"outputs",
|
|
7631
|
+
"format",
|
|
7632
|
+
"content",
|
|
7633
|
+
"description",
|
|
7634
|
+
"tags",
|
|
7635
|
+
"is_template",
|
|
7636
|
+
"version",
|
|
7637
|
+
"created_at",
|
|
7638
|
+
"updated_at",
|
|
7639
|
+
"synced_at"
|
|
7640
|
+
];
|
|
7641
|
+
function parseConfigListDetail(value) {
|
|
7642
|
+
if (value === undefined)
|
|
7643
|
+
return;
|
|
7644
|
+
if (value === "compact" || value === "full")
|
|
7645
|
+
return value;
|
|
7646
|
+
throw new Error('--detail must be "compact" or "full"');
|
|
7647
|
+
}
|
|
7648
|
+
function parseConfigListFields(value, detail) {
|
|
7649
|
+
if (value === undefined)
|
|
7650
|
+
return;
|
|
7651
|
+
const raw = String(value);
|
|
7652
|
+
if (raw.length > 1024)
|
|
7653
|
+
throw new Error("--fields exceeds 1024 characters");
|
|
7654
|
+
const fields = raw.split(",").map((field) => field.trim());
|
|
7655
|
+
if (fields.length === 0 || fields.some((field) => field.length === 0)) {
|
|
7656
|
+
throw new Error("--fields must contain non-empty comma-separated field names");
|
|
7657
|
+
}
|
|
7658
|
+
if (fields.length > 32)
|
|
7659
|
+
throw new Error("--fields accepts at most 32 fields");
|
|
7660
|
+
if (new Set(fields).size !== fields.length)
|
|
7661
|
+
throw new Error("--fields must not contain duplicate names");
|
|
7662
|
+
const allowed = new Set(detail === "full" ? FULL_CONFIG_FIELDS : [...COMPACT_CONFIG_FIELDS, ...OPTIONAL_CONFIG_IDENTITY_FIELDS]);
|
|
7663
|
+
const unknown = fields.filter((field) => !allowed.has(field));
|
|
7664
|
+
if (unknown.length > 0)
|
|
7665
|
+
throw new Error(`Unknown ${detail} config field(s): ${unknown.join(", ")}`);
|
|
7666
|
+
if (!fields.includes("id"))
|
|
7667
|
+
throw new Error("--fields must include the immutable id field");
|
|
7668
|
+
if (detail !== "full" && fields.includes("content")) {
|
|
7669
|
+
throw new Error("Config content requires --detail full");
|
|
7670
|
+
}
|
|
7671
|
+
return fields;
|
|
7672
|
+
}
|
|
7673
|
+
function projectConfigFields(record, fields) {
|
|
7674
|
+
return Object.fromEntries(fields.map((field) => [field, record[field]]));
|
|
7675
|
+
}
|
|
7676
|
+
async function readAllConfigIdentityPages(store, filter) {
|
|
7677
|
+
const items = [];
|
|
7678
|
+
const seen = new Set;
|
|
7679
|
+
let cursor = 0;
|
|
7680
|
+
let expectedTotal = null;
|
|
7681
|
+
let sourceBounded = true;
|
|
7682
|
+
let previousId = null;
|
|
7683
|
+
while (true) {
|
|
7684
|
+
const page = await store.listConfigIdentitiesPage(filter, { limit: 100, cursor });
|
|
7685
|
+
sourceBounded = sourceBounded && page.source_bounded;
|
|
7686
|
+
if (expectedTotal === null)
|
|
7687
|
+
expectedTotal = page.total;
|
|
7688
|
+
else if (page.total !== expectedTotal)
|
|
7689
|
+
throw new Error("Config collection changed while paging");
|
|
7690
|
+
for (const item of page.items) {
|
|
7691
|
+
if (seen.has(item.id))
|
|
7692
|
+
throw new Error(`Config collection repeated ${item.id} while paging`);
|
|
7693
|
+
if (page.source_bounded && previousId !== null && item.id <= previousId)
|
|
7694
|
+
throw new Error("Config collection order changed while paging");
|
|
7695
|
+
seen.add(item.id);
|
|
7696
|
+
previousId = page.source_bounded ? item.id : null;
|
|
7697
|
+
items.push(item);
|
|
7698
|
+
}
|
|
7699
|
+
if (page.next_cursor === null)
|
|
7700
|
+
break;
|
|
7701
|
+
if (page.next_cursor <= cursor)
|
|
7702
|
+
throw new Error("Config identity pagination did not advance");
|
|
7703
|
+
cursor = page.next_cursor;
|
|
7704
|
+
}
|
|
7705
|
+
if (expectedTotal !== null && items.length !== expectedTotal) {
|
|
7706
|
+
throw new Error(`Config collection returned ${items.length} of ${expectedTotal} rows`);
|
|
7707
|
+
}
|
|
7708
|
+
return { items, source_bounded: sourceBounded };
|
|
7709
|
+
}
|
|
7710
|
+
async function readAllConfigPages(store, filter) {
|
|
7711
|
+
const items = [];
|
|
7712
|
+
const seen = new Set;
|
|
7713
|
+
let cursor = 0;
|
|
7714
|
+
let expectedTotal = null;
|
|
7715
|
+
let sourceBounded = true;
|
|
7716
|
+
let previousId = null;
|
|
7717
|
+
while (true) {
|
|
7718
|
+
const page = await store.listConfigsPage(filter, { limit: 100, cursor });
|
|
7719
|
+
sourceBounded = sourceBounded && page.source_bounded;
|
|
7720
|
+
if (expectedTotal === null)
|
|
7721
|
+
expectedTotal = page.total;
|
|
7722
|
+
else if (page.total !== expectedTotal)
|
|
7723
|
+
throw new Error("Config collection changed while paging");
|
|
7724
|
+
for (const item of page.items) {
|
|
7725
|
+
if (seen.has(item.id))
|
|
7726
|
+
throw new Error(`Config collection repeated ${item.id} while paging`);
|
|
7727
|
+
if (page.source_bounded && previousId !== null && item.id <= previousId)
|
|
7728
|
+
throw new Error("Config collection order changed while paging");
|
|
7729
|
+
seen.add(item.id);
|
|
7730
|
+
previousId = page.source_bounded ? item.id : null;
|
|
7731
|
+
items.push(item);
|
|
7732
|
+
}
|
|
7733
|
+
if (page.next_cursor === null)
|
|
7734
|
+
break;
|
|
7735
|
+
if (page.next_cursor <= cursor)
|
|
7736
|
+
throw new Error("Config pagination did not advance");
|
|
7737
|
+
cursor = page.next_cursor;
|
|
7738
|
+
}
|
|
7739
|
+
if (expectedTotal !== null && items.length !== expectedTotal) {
|
|
7740
|
+
throw new Error(`Config collection returned ${items.length} of ${expectedTotal} rows`);
|
|
7741
|
+
}
|
|
7742
|
+
return { items, source_bounded: sourceBounded };
|
|
7743
|
+
}
|
|
7744
|
+
function configListEnvelope(records, page, detail, fields) {
|
|
7745
|
+
const complete = page.cursor === 0 && !page.has_more;
|
|
7746
|
+
const truncated = !complete;
|
|
7747
|
+
const truncationReason = page.cursor > 0 ? page.has_more ? "cursor_and_limit" : "cursor" : page.has_more ? "limit" : null;
|
|
7748
|
+
return {
|
|
7749
|
+
configs: records,
|
|
7750
|
+
_meta: {
|
|
7751
|
+
schema_version: 1,
|
|
7752
|
+
count: records.length,
|
|
7753
|
+
total: page.total,
|
|
7754
|
+
limit: page.limit,
|
|
7755
|
+
cursor: page.cursor,
|
|
7756
|
+
next_cursor: page.next_cursor,
|
|
7757
|
+
has_more: page.has_more,
|
|
7758
|
+
complete,
|
|
7759
|
+
truncated,
|
|
7760
|
+
truncation_reason: truncationReason,
|
|
7761
|
+
detail,
|
|
7762
|
+
fields: [...fields],
|
|
7763
|
+
source_bounded: page.source_bounded
|
|
7764
|
+
}
|
|
7765
|
+
};
|
|
7766
|
+
}
|
|
7767
|
+
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) => {
|
|
7768
|
+
const requestedDetail = parseConfigListDetail(opts.detail);
|
|
7769
|
+
const modernJson = requestedDetail !== undefined || opts.fields !== undefined;
|
|
7770
|
+
const jsonRequested = Boolean(opts.json || opts.format === "json");
|
|
7771
|
+
if (modernJson && !jsonRequested)
|
|
7772
|
+
throw new Error("--detail and --fields require JSON output");
|
|
7773
|
+
if (opts.all && !modernJson)
|
|
7774
|
+
throw new Error("--all requires JSON output with --detail or --fields");
|
|
7775
|
+
if (opts.pretty && !jsonRequested)
|
|
7776
|
+
throw new Error("--pretty requires JSON output");
|
|
7777
|
+
const detail = requestedDetail ?? "compact";
|
|
7778
|
+
const fields = parseConfigListFields(opts.fields, detail) ?? (detail === "full" ? [...FULL_CONFIG_FIELDS] : [...COMPACT_CONFIG_FIELDS]);
|
|
7779
|
+
const filter = {
|
|
7195
7780
|
category: opts.category,
|
|
7196
7781
|
agent: opts.agent,
|
|
7197
7782
|
kind: opts.kind,
|
|
7198
7783
|
tags: opts.tag ? [opts.tag] : undefined,
|
|
7199
7784
|
search: opts.search
|
|
7200
|
-
}
|
|
7785
|
+
};
|
|
7786
|
+
const store = resolveConfigStore();
|
|
7787
|
+
if (modernJson) {
|
|
7788
|
+
if (opts.all && (opts.cursor !== undefined || opts.limit !== undefined)) {
|
|
7789
|
+
throw new Error("--all cannot be combined with --limit or --cursor");
|
|
7790
|
+
}
|
|
7791
|
+
let page2;
|
|
7792
|
+
if (opts.all) {
|
|
7793
|
+
if (detail === "full") {
|
|
7794
|
+
const completeRead = await readAllConfigPages(store, filter);
|
|
7795
|
+
page2 = {
|
|
7796
|
+
items: completeRead.items,
|
|
7797
|
+
total: completeRead.items.length,
|
|
7798
|
+
limit: Math.max(completeRead.items.length, 1),
|
|
7799
|
+
cursor: 0,
|
|
7800
|
+
next_cursor: null,
|
|
7801
|
+
has_more: false,
|
|
7802
|
+
complete: true,
|
|
7803
|
+
truncated: false,
|
|
7804
|
+
source_bounded: completeRead.source_bounded
|
|
7805
|
+
};
|
|
7806
|
+
} else {
|
|
7807
|
+
const completeRead = await readAllConfigIdentityPages(store, filter);
|
|
7808
|
+
page2 = {
|
|
7809
|
+
items: completeRead.items,
|
|
7810
|
+
total: completeRead.items.length,
|
|
7811
|
+
limit: Math.max(completeRead.items.length, 1),
|
|
7812
|
+
cursor: 0,
|
|
7813
|
+
next_cursor: null,
|
|
7814
|
+
has_more: false,
|
|
7815
|
+
complete: true,
|
|
7816
|
+
truncated: false,
|
|
7817
|
+
source_bounded: completeRead.source_bounded
|
|
7818
|
+
};
|
|
7819
|
+
}
|
|
7820
|
+
} else {
|
|
7821
|
+
page2 = detail === "full" ? await store.listConfigsPage(filter, { limit: opts.limit, cursor: opts.cursor }) : await store.listConfigIdentitiesPage(filter, { limit: opts.limit, cursor: opts.cursor });
|
|
7822
|
+
}
|
|
7823
|
+
const records = page2.items.map((item) => projectConfigFields(item, fields));
|
|
7824
|
+
printMachineJson(configListEnvelope(records, {
|
|
7825
|
+
total: page2.total,
|
|
7826
|
+
limit: opts.all ? null : page2.limit,
|
|
7827
|
+
cursor: page2.cursor,
|
|
7828
|
+
next_cursor: page2.next_cursor,
|
|
7829
|
+
has_more: page2.has_more,
|
|
7830
|
+
complete: page2.complete,
|
|
7831
|
+
source_bounded: page2.source_bounded
|
|
7832
|
+
}, detail, fields), Boolean(opts.pretty));
|
|
7833
|
+
return;
|
|
7834
|
+
}
|
|
7835
|
+
const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
|
|
7201
7836
|
if (fmt === "json") {
|
|
7837
|
+
const configs = opts.limit !== undefined || opts.cursor !== undefined ? (await store.listConfigsPage(filter, { limit: opts.limit, cursor: opts.cursor })).items : await store.listConfigs(filter);
|
|
7202
7838
|
printJson(configs);
|
|
7203
7839
|
return;
|
|
7204
7840
|
}
|
|
7205
|
-
|
|
7841
|
+
const page = opts.all ? (() => {
|
|
7842
|
+
throw new Error("--all is only available with --json and --detail");
|
|
7843
|
+
})() : await store.listConfigSummariesPage(filter, { limit: opts.limit, cursor: opts.cursor });
|
|
7844
|
+
if (page.total === 0) {
|
|
7206
7845
|
console.log(chalk.dim("No configs found."));
|
|
7207
7846
|
return;
|
|
7208
7847
|
}
|
|
7209
|
-
const page = paginate(configs, { limit: opts.limit, cursor: opts.cursor });
|
|
7210
7848
|
if (fmt === "compact") {
|
|
7211
7849
|
printConfigRows(page.items);
|
|
7212
7850
|
} else {
|
|
@@ -7215,7 +7853,7 @@ program.command("list").alias("ls").description("List stored configs").option("-
|
|
|
7215
7853
|
console.log();
|
|
7216
7854
|
}
|
|
7217
7855
|
}
|
|
7218
|
-
pageFooter("configs list", page, "Use --verbose for expanded rows, --json for
|
|
7856
|
+
pageFooter("configs list", page, "Use --verbose for expanded rows, --json --detail compact for metadata, or `configs show <slug>` for content.");
|
|
7219
7857
|
});
|
|
7220
7858
|
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) => {
|
|
7221
7859
|
try {
|
|
@@ -7260,13 +7898,18 @@ program.command("tag <id>").description("Add or remove tags on a stored config (
|
|
|
7260
7898
|
}
|
|
7261
7899
|
console.log(chalk.green("\u2713") + ` Tags on ${chalk.bold(updated.name)} ${chalk.dim(`(${updated.slug})`)}: ${nextTags.join(", ") || chalk.dim("(none)")}`);
|
|
7262
7900
|
});
|
|
7263
|
-
program.command("add <path>").description("Ingest a file into the config DB").option("-n, --name <name>", "config name (defaults to filename)").option("-c, --category <cat>", "category override").option("-a, --agent <agent>", "agent override").option("-k, --kind <kind>", "kind: file|reference", "file").option("--template", "mark as template (has {{VAR}} placeholders)").option("--update", "if a config already owns this path, update that row in place instead of refusing").action(async (filePath, opts) => {
|
|
7264
|
-
const
|
|
7901
|
+
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").option("--expected-version <version>", "with --update, require the stored version to match atomically").action(async (filePath, opts) => {
|
|
7902
|
+
const expectedVersion = opts.expectedVersion === undefined ? undefined : Number(opts.expectedVersion);
|
|
7903
|
+
validateExpectedConfigVersion(expectedVersion);
|
|
7904
|
+
if (expectedVersion !== undefined && !opts.update) {
|
|
7905
|
+
throw new Error("--expected-version requires --update");
|
|
7906
|
+
}
|
|
7907
|
+
const abs = resolve7(filePath);
|
|
7265
7908
|
if (!existsSync11(abs)) {
|
|
7266
7909
|
console.error(chalk.red(`File not found: ${abs}`));
|
|
7267
7910
|
process.exit(1);
|
|
7268
7911
|
}
|
|
7269
|
-
const rawContent =
|
|
7912
|
+
const rawContent = readFileSync8(abs, "utf-8");
|
|
7270
7913
|
const storedFmt = detectFormat(abs);
|
|
7271
7914
|
const fmt = redactFormatForTarget(abs, storedFmt);
|
|
7272
7915
|
const { content, redacted, isTemplate } = redactContent(rawContent, fmt);
|
|
@@ -7297,10 +7940,11 @@ program.command("add <path>").description("Ingest a file into the config DB").op
|
|
|
7297
7940
|
const exactIndex = isReference ? existingOwners.findIndex((owner) => owner.name === name) : -1;
|
|
7298
7941
|
const target = exactIndex >= 0 ? existingOwners[exactIndex] : existingOwners[0];
|
|
7299
7942
|
const rest = existingOwners.filter((owner) => owner.id !== target.id);
|
|
7300
|
-
if (content !== target.content) {
|
|
7943
|
+
if (content !== target.content && expectedVersion === undefined) {
|
|
7301
7944
|
await store.createSnapshot(target.id, target.content, target.version);
|
|
7302
7945
|
}
|
|
7303
7946
|
config = await store.updateConfig(target.id, {
|
|
7947
|
+
expected_version: expectedVersion,
|
|
7304
7948
|
content,
|
|
7305
7949
|
format: storedFmt,
|
|
7306
7950
|
is_template: (opts.template ?? false) || isTemplate,
|
|
@@ -7317,6 +7961,9 @@ program.command("add <path>").description("Ingest a file into the config DB").op
|
|
|
7317
7961
|
}
|
|
7318
7962
|
return;
|
|
7319
7963
|
}
|
|
7964
|
+
if (expectedVersion !== undefined) {
|
|
7965
|
+
throw new Error("--expected-version requires an existing config; no config was created");
|
|
7966
|
+
}
|
|
7320
7967
|
config = await store.createConfig({
|
|
7321
7968
|
name,
|
|
7322
7969
|
kind: opts.kind ?? "file",
|
|
@@ -7445,7 +8092,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
|
|
|
7445
8092
|
for (const entry of entries) {
|
|
7446
8093
|
if (!entry.isDirectory())
|
|
7447
8094
|
continue;
|
|
7448
|
-
const projDir =
|
|
8095
|
+
const projDir = join11(absDir, entry.name);
|
|
7449
8096
|
const hasAgentConfig = [
|
|
7450
8097
|
"CLAUDE.md",
|
|
7451
8098
|
".mcp.json",
|
|
@@ -7458,7 +8105,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
|
|
|
7458
8105
|
".aicopilot",
|
|
7459
8106
|
".cursor",
|
|
7460
8107
|
".agents"
|
|
7461
|
-
].some((marker) => existsSync11(
|
|
8108
|
+
].some((marker) => existsSync11(join11(projDir, marker)));
|
|
7462
8109
|
if (!hasAgentConfig)
|
|
7463
8110
|
continue;
|
|
7464
8111
|
const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun, store });
|
|
@@ -7506,7 +8153,7 @@ program.command("import <file>").description("Restore a v2 Instructions domain a
|
|
|
7506
8153
|
});
|
|
7507
8154
|
program.command("whoami").description("Show setup summary").action(async () => {
|
|
7508
8155
|
const store = resolveConfigStore();
|
|
7509
|
-
const dbPath = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] ||
|
|
8156
|
+
const dbPath = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join11(getRawStoreRoot(), "instructions.db");
|
|
7510
8157
|
const stats = await store.getConfigStats();
|
|
7511
8158
|
console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
|
|
7512
8159
|
console.log(chalk.cyan(store.mode === "api" ? "API:" : "DB:") + " " + dbPath);
|
|
@@ -7890,7 +8537,7 @@ projectContextCmd.command("apply").description("Atomically write project context
|
|
|
7890
8537
|
}
|
|
7891
8538
|
});
|
|
7892
8539
|
var sessionCmd = program.command("session").description("Plan and apply session-scoped agent instruction files");
|
|
7893
|
-
sessionCmd.command("plan").description("Produce a dry-run render plan for profile-scoped instruction injection").requiredOption("--tool <tool>", `target tool (${SESSION_RENDER_TOOLS.join("|")})`).requiredOption("--profile <profile>", "account/profile name that owns the rendered instruction home").option("--target-home <path>", "native instruction home; Grok: GROK_HOME
|
|
8540
|
+
sessionCmd.command("plan").description("Produce a dry-run render plan for profile-scoped instruction injection").requiredOption("--tool <tool>", `target tool (${SESSION_RENDER_TOOLS.join("|")})`).requiredOption("--profile <profile>", "account/profile name that owns the rendered instruction home").option("--target-home <path>", "native instruction home; Sumi: resolved config from `sumi debug paths config`; Grok: GROK_HOME; Devin: user config directory (global AGENTS.md unless --project-root is set)").option("--project-root <path>", "repository root for project-scoped adapters such as Cursor").option("--session-id <id>", "session id to include in the manifest").option("--source <layer:id=path>", `instruction source file; layers: ${SESSION_SOURCE_LAYER_HELP}`, collectOption, []).option("--config <layer:id-or-slug>", "stored config source by id/slug; repeatable; layer aliases match --source", collectOption, []).option("--identity-export <path>", "OpenIdentities configs instruction export JSON; repeatable", collectOption, []).option("--replace-source <replacer-id>[=<target-source-id>]", "source id that broadly replaces earlier layers, or targets one earlier source", collectOption, []).option("--compile-profile <id-or-slug>", "compile persisted config bindings from this Instructions profile").option("--provider-version <semver>", "installed provider version used for capability matching").option("--provider-variant <variant>", "explicit provider capability variant (for example OpenCode v2-agents)").option("--model <model>", "active model used for model activation").option("--path <path>", "active path recorded in the graph context").option("--manual <config-id-or-slug>", "manually activate a binding; repeatable", collectOption, []).option("--asset-surface <surface>", "explicit provider asset surface (for example cline cli or ide)").option("--asset-scope <scope>", "asset scope (global|project|session)").option("--allow-asset-installers", "opt in to planned provider installers; planning never invokes them").option("--codewith-native-imports", "select the gated Codewith native @ import adapter").option("--allow-empty-sources", "allow an explicit empty render plan").option("--check-global-coverage", "verify every registered, non-retired global-* source is in this render (expected read fresh from the registry, independent of this plan); on shortfall warn, exit non-zero, and for apply refuse to write (todos 102d6d0a, O15-00694)").option("--no-station-profile", "do not inject the cached station-profile source").option("--json", "output dry-run JSON").action(async (opts) => {
|
|
7894
8541
|
try {
|
|
7895
8542
|
const tool = opts.tool;
|
|
7896
8543
|
if (!SESSION_RENDER_TOOLS.includes(tool)) {
|
|
@@ -7945,7 +8592,7 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
|
|
|
7945
8592
|
process.exit(1);
|
|
7946
8593
|
}
|
|
7947
8594
|
});
|
|
7948
|
-
sessionCmd.command("apply").description("Write a session render plan to its managed target home or explicit project root").requiredOption("--tool <tool>", `target tool (${SESSION_RENDER_TOOLS.join("|")})`).requiredOption("--profile <profile>", "account/profile name that owns the rendered instruction home").option("--target-home <path>", "native instruction home; Grok: GROK_HOME
|
|
8595
|
+
sessionCmd.command("apply").description("Write a session render plan to its managed target home or explicit project root").requiredOption("--tool <tool>", `target tool (${SESSION_RENDER_TOOLS.join("|")})`).requiredOption("--profile <profile>", "account/profile name that owns the rendered instruction home").option("--target-home <path>", "native instruction home; Sumi: resolved config from `sumi debug paths config`; Grok: GROK_HOME; Devin: user config directory (global AGENTS.md unless --project-root is set)").option("--project-root <path>", "repository root for project-scoped adapters such as Cursor").option("--session-id <id>", "session id to include in the manifest").option("--source <layer:id=path>", `instruction source file; layers: ${SESSION_SOURCE_LAYER_HELP}`, collectOption, []).option("--config <layer:id-or-slug>", "stored config source by id/slug; repeatable; layer aliases match --source", collectOption, []).option("--identity-export <path>", "OpenIdentities configs instruction export JSON; repeatable", collectOption, []).option("--replace-source <replacer-id>[=<target-source-id>]", "source id that broadly replaces earlier layers, or targets one earlier source", collectOption, []).option("--compile-profile <id-or-slug>", "compile persisted config bindings from this Instructions profile").option("--provider-version <semver>", "installed provider version used for capability matching").option("--provider-variant <variant>", "explicit provider capability variant (for example OpenCode v2-agents)").option("--model <model>", "active model used for model activation").option("--path <path>", "active path recorded in the graph context").option("--manual <config-id-or-slug>", "manually activate a binding; repeatable", collectOption, []).option("--asset-surface <surface>", "explicit provider asset surface (for example cline cli or ide)").option("--asset-scope <scope>", "asset scope (global|project|session)").option("--codewith-native-imports", "select the gated Codewith native @ import adapter").option("--allow-empty-sources", "allow an explicit empty render").option("--check-global-coverage", "verify every registered, non-retired global-* source is in this render (expected read fresh from the registry, independent of this plan); on shortfall warn, exit non-zero, and for apply refuse to write (todos 102d6d0a, O15-00694)").option("--no-station-profile", "do not inject the cached station-profile source").option("--dry-run", "preview writes and conflicts without writing").option("--force", "overwrite existing unmanaged files").option("--adopt-file <relativepath=sha256>", "adopt one reviewed unmanaged generated target only at its exact observed SHA256; repeatable", collectOption, []).option("--reconcile-file <relativepath=sha256>", "reconcile one reviewed managed drifted target at its exact observed SHA256; requires --expected-manifest-sha256", collectOption, []).option("--expected-manifest-sha256 <sha256>", "require the previously reviewed manifest preimage to match exactly before applying").option("--json", "output apply JSON").action(async (opts) => {
|
|
7949
8596
|
try {
|
|
7950
8597
|
const tool = opts.tool;
|
|
7951
8598
|
if (!SESSION_RENDER_TOOLS.includes(tool)) {
|
|
@@ -7971,7 +8618,15 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
|
|
|
7971
8618
|
return;
|
|
7972
8619
|
}
|
|
7973
8620
|
const ownedClaudeAuthorities = tool === "claude" ? await loadOwnedClaudeAuthorities(store) : undefined;
|
|
7974
|
-
const
|
|
8621
|
+
const parsePreimages = (values, option) => values.map((value) => {
|
|
8622
|
+
const separator = value.lastIndexOf("=");
|
|
8623
|
+
if (separator <= 0)
|
|
8624
|
+
throw new Error(`${option} requires relativepath=sha256.`);
|
|
8625
|
+
return { relativePath: value.slice(0, separator), sha256: value.slice(separator + 1) };
|
|
8626
|
+
});
|
|
8627
|
+
const adoptFiles = parsePreimages(opts.adoptFile, "--adopt-file");
|
|
8628
|
+
const reconcileFiles = parsePreimages(opts.reconcileFile, "--reconcile-file");
|
|
8629
|
+
const result = applySessionRender(plan, { dryRun: opts.dryRun, force: opts.force, adoptFiles, reconcileFiles, expectedManifestSha256: opts.expectedManifestSha256, ownedClaudeAuthorities });
|
|
7975
8630
|
if (opts.json) {
|
|
7976
8631
|
printJson({
|
|
7977
8632
|
...result,
|
|
@@ -8012,7 +8667,7 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
|
|
|
8012
8667
|
console.log(chalk.dim(`global source coverage: ${globalCoverage.expectedSlugs.length}/${globalCoverage.expectedSlugs.length} complete`));
|
|
8013
8668
|
}
|
|
8014
8669
|
if (result.conflicts.length > 0) {
|
|
8015
|
-
console.error(chalk.red(`Conflicts: ${result.conflicts.length}.
|
|
8670
|
+
console.error(chalk.red(`Conflicts: ${result.conflicts.length}. Review and import existing instructions before adopting each exact preimage with --adopt-file.`));
|
|
8016
8671
|
process.exitCode = 1;
|
|
8017
8672
|
}
|
|
8018
8673
|
} catch (e) {
|
|
@@ -8020,6 +8675,29 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
|
|
|
8020
8675
|
process.exit(1);
|
|
8021
8676
|
}
|
|
8022
8677
|
});
|
|
8678
|
+
sessionCmd.command("refresh").description("Fetch the recorded hosted profile, recompile and safely update a managed target; unchanged output is not rewritten").requiredOption("--target-home <path>", "existing managed global or project target containing its hosted profile selector").option("--dry-run", "fetch authoritative sources and preview without writing").option("--json", "output refresh receipt without instruction content").action(async (opts) => {
|
|
8679
|
+
try {
|
|
8680
|
+
const result = await refreshSessionRender({ targetHome: resolveSessionPath(opts.targetHome), store: resolveConfigStore(), dryRun: opts.dryRun });
|
|
8681
|
+
if (opts.json)
|
|
8682
|
+
printJson(result);
|
|
8683
|
+
else {
|
|
8684
|
+
console.log(`${result.status}: ${result.targetHome}`);
|
|
8685
|
+
console.log(`hosted profile: ${result.selector.profileId}`);
|
|
8686
|
+
console.log(`source hash: ${result.sourceHash}`);
|
|
8687
|
+
if (result.apply.snapshotPath)
|
|
8688
|
+
console.log(`snapshot: ${result.apply.snapshotPath}`);
|
|
8689
|
+
}
|
|
8690
|
+
if (result.status === "blocked")
|
|
8691
|
+
process.exitCode = 1;
|
|
8692
|
+
} catch (error) {
|
|
8693
|
+
const message = formatCliError(error);
|
|
8694
|
+
if (opts.json)
|
|
8695
|
+
printJson({ status: "failed", error: message });
|
|
8696
|
+
else
|
|
8697
|
+
console.error(chalk.red(message));
|
|
8698
|
+
process.exitCode = 1;
|
|
8699
|
+
}
|
|
8700
|
+
});
|
|
8023
8701
|
sessionCmd.command("restore <snapshot>").description("Restore a session render snapshot only when applied files have not drifted").option("--dry-run", "preview restore actions and conflicts without writing").option("--json", "output restore JSON").action(async (snapshot, opts) => {
|
|
8024
8702
|
try {
|
|
8025
8703
|
const result = restoreSessionRenderSnapshot(resolveSessionPath(snapshot), {
|
|
@@ -8214,7 +8892,7 @@ templateCmd.command("vars <id>").description("Show template variables").action(a
|
|
|
8214
8892
|
});
|
|
8215
8893
|
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) => {
|
|
8216
8894
|
try {
|
|
8217
|
-
const { renderTemplate } = await import("../chunks/template-
|
|
8895
|
+
const { renderTemplate } = await import("../chunks/template-e4qrxs8p.js");
|
|
8218
8896
|
const c = await resolveConfigStore().getConfig(id);
|
|
8219
8897
|
const vars = {};
|
|
8220
8898
|
if (opts.var) {
|
|
@@ -8228,7 +8906,7 @@ templateCmd.command("render <id>").description("Render a template config with va
|
|
|
8228
8906
|
}
|
|
8229
8907
|
}
|
|
8230
8908
|
if (opts.env) {
|
|
8231
|
-
const { extractTemplateVars: extractTemplateVars2 } = await import("../chunks/template-
|
|
8909
|
+
const { extractTemplateVars: extractTemplateVars2 } = await import("../chunks/template-e4qrxs8p.js");
|
|
8232
8910
|
for (const v of extractTemplateVars2(c.content)) {
|
|
8233
8911
|
if (!(v.name in vars) && process.env[v.name]) {
|
|
8234
8912
|
vars[v.name] = process.env[v.name];
|
|
@@ -8277,7 +8955,7 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
|
|
|
8277
8955
|
} else if (opts.all) {
|
|
8278
8956
|
configs = await store.listConfigs(opts.category ? { kind: "file", category: opts.category } : { kind: "file" });
|
|
8279
8957
|
} else {
|
|
8280
|
-
const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await import("../chunks/sync-
|
|
8958
|
+
const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await import("../chunks/sync-azd9t4p8.js");
|
|
8281
8959
|
const slugs = [
|
|
8282
8960
|
...KNOWN_CONFIGS2.filter((k) => !k.rulesDir).map((k) => k.name)
|
|
8283
8961
|
];
|
|
@@ -8571,7 +9249,7 @@ command = "${mcpBinary}"
|
|
|
8571
9249
|
args = []
|
|
8572
9250
|
`;
|
|
8573
9251
|
if (ex(configPath)) {
|
|
8574
|
-
const content =
|
|
9252
|
+
const content = readFileSync8(configPath, "utf-8");
|
|
8575
9253
|
if (findTomlTableLines(content.split(`
|
|
8576
9254
|
`), "mcp_servers.configs")) {
|
|
8577
9255
|
console.log(chalk.dim("= Already installed in Codex"));
|
|
@@ -8633,7 +9311,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
|
|
|
8633
9311
|
console.log(chalk.dim("= Not installed in Codex (no config.toml)"));
|
|
8634
9312
|
continue;
|
|
8635
9313
|
}
|
|
8636
|
-
const content =
|
|
9314
|
+
const content = readFileSync8(configPath, "utf-8");
|
|
8637
9315
|
const lines = content.split(`
|
|
8638
9316
|
`);
|
|
8639
9317
|
const table = findTomlTableLines(lines, "mcp_servers.configs");
|
|
@@ -8652,7 +9330,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
|
|
|
8652
9330
|
console.log(chalk.green("\u2713") + " Removed from Codex");
|
|
8653
9331
|
} else if (target === "antigravity") {
|
|
8654
9332
|
const { existsSync: ex, readFileSync: rf, writeFileSync: wf } = await import("fs");
|
|
8655
|
-
const configPath =
|
|
9333
|
+
const configPath = join11(homedir4(), ".gemini", "config", "mcp_config.json");
|
|
8656
9334
|
if (!ex(configPath)) {
|
|
8657
9335
|
console.log(chalk.dim("= Not installed in Antigravity (no mcp_config.json)"));
|
|
8658
9336
|
continue;
|
|
@@ -8687,15 +9365,15 @@ program.command("migrate-legacy").description("Migrate the historical ~/.hasna/c
|
|
|
8687
9365
|
if (!opts.confirmLocal) {
|
|
8688
9366
|
throw new Error("migrate-legacy requires --confirm-local");
|
|
8689
9367
|
}
|
|
8690
|
-
const destinationPath =
|
|
9368
|
+
const destinationPath = resolve7(opts.destination || process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join11(getRawStoreRoot(), "instructions.db"));
|
|
8691
9369
|
const database = await import("../chunks/database-wstmr56z.js");
|
|
8692
9370
|
database.getDatabase(destinationPath);
|
|
8693
9371
|
database.resetDatabase();
|
|
8694
9372
|
const { migrateLegacyInstructionsStore } = await import("../chunks/legacy-store-migration-z2m5yzyk.js");
|
|
8695
9373
|
const result = migrateLegacyInstructionsStore({
|
|
8696
|
-
...opts.source ? { sourcePath:
|
|
9374
|
+
...opts.source ? { sourcePath: resolve7(opts.source) } : {},
|
|
8697
9375
|
destinationPath,
|
|
8698
|
-
...opts.backup ? { backupPath:
|
|
9376
|
+
...opts.backup ? { backupPath: resolve7(opts.backup) } : {},
|
|
8699
9377
|
dryRun: !opts.apply,
|
|
8700
9378
|
...opts.mergePreserveDestination ? { mergePolicy: "preserve-destination" } : {},
|
|
8701
9379
|
operatorConfirmed: true
|
|
@@ -8714,7 +9392,7 @@ program.command("migrate-legacy").description("Migrate the historical ~/.hasna/c
|
|
|
8714
9392
|
if (result.backupCreated)
|
|
8715
9393
|
console.log(chalk.dim(" destination backup created before mutation"));
|
|
8716
9394
|
});
|
|
8717
|
-
program.command("init").description("First-time setup:
|
|
9395
|
+
program.command("init").description("First-time setup: create default profiles; local file import into a hosted authority is explicit").option("--force", "delete existing DB and start fresh").option("--import-local", "explicitly import known local config files into the configured hosted authority").action(async (opts) => {
|
|
8718
9396
|
const store = resolveConfigStore();
|
|
8719
9397
|
if (opts.force) {
|
|
8720
9398
|
try {
|
|
@@ -8728,26 +9406,28 @@ program.command("init").description("First-time setup: sync all known configs, c
|
|
|
8728
9406
|
}
|
|
8729
9407
|
console.log(chalk.bold(`@hasna/instructions \u2014 initializing
|
|
8730
9408
|
`));
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
9409
|
+
if (store.mode === "local" || opts.importLocal) {
|
|
9410
|
+
const result = await syncKnown({ store });
|
|
9411
|
+
console.log(chalk.green("\u2713") + ` Synced: +${result.added} updated:${result.updated} unchanged:${result.unchanged}`);
|
|
9412
|
+
if (result.skipped.length > 0)
|
|
9413
|
+
console.log(chalk.dim(" skipped: " + result.skipped.join(", ")));
|
|
9414
|
+
} else {
|
|
9415
|
+
console.log(chalk.dim("Hosted authority retained; use --import-local for an intentional local-file migration."));
|
|
8735
9416
|
}
|
|
8736
9417
|
const refs = [
|
|
8737
9418
|
{ slug: "workspace-structure", name: "Workspace Structure", category: "workspace", content: `# Workspace Structure
|
|
8738
9419
|
|
|
8739
|
-
|
|
8740
|
-
{ slug: "secrets-schema", name: "Secrets Schema", category: "secrets_schema", content: `#
|
|
8741
|
-
|
|
8742
|
-
Location: ~/.secrets (sourced by ~/.zshrc)
|
|
8743
|
-
Format: export KEY_NAME="value"
|
|
9420
|
+
Resolve repositories and task worktrees through the configured repository manager. Use the configured scratch workspace for temporary execution artifacts. Keep durable project records in their owning application and follow the current project conventions.`, desc: "Repository, worktree, and execution scratch responsibilities" },
|
|
9421
|
+
{ slug: "secrets-schema", name: "Secrets Schema", category: "secrets_schema", content: `# Credential References
|
|
8744
9422
|
|
|
8745
|
-
|
|
9423
|
+
Store credential values in the configured Secrets authority. Application configuration records contain credential names and supported resolver references only. Resolve values through the approved consuming application at execution time; never embed values in instruction content, generated prompts, or shell profiles.`, desc: "Secret reference and runtime resolution conventions; no credential values" }
|
|
8746
9424
|
];
|
|
8747
9425
|
for (const ref of refs) {
|
|
8748
9426
|
try {
|
|
8749
9427
|
await store.getConfig(ref.slug);
|
|
8750
|
-
} catch {
|
|
9428
|
+
} catch (error) {
|
|
9429
|
+
if (!(error instanceof ConfigNotFoundError))
|
|
9430
|
+
throw error;
|
|
8751
9431
|
await store.createConfig({ name: ref.name, category: ref.category, agent: "global", format: "markdown", content: ref.content, kind: "reference", description: ref.desc });
|
|
8752
9432
|
}
|
|
8753
9433
|
}
|
|
@@ -8757,12 +9437,11 @@ Keys: ANTHROPIC_API_KEY, OPENAI_API_KEY, EXA_API_KEY, NPM_TOKEN, GITHUB_TOKEN`,
|
|
|
8757
9437
|
await ensureProjectDashboardStandardConfig(store);
|
|
8758
9438
|
try {
|
|
8759
9439
|
await store.getProfile("my-setup");
|
|
8760
|
-
} catch {
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
console.log(chalk.green("\u2713") + ` Created profile "my-setup" with ${allConfigs.length} configs`);
|
|
9440
|
+
} catch (error) {
|
|
9441
|
+
if (!(error instanceof ProfileNotFoundError))
|
|
9442
|
+
throw error;
|
|
9443
|
+
await store.createProfile({ name: "my-setup", description: "Instruction profile; add explicitly reviewed and scoped source bindings" });
|
|
9444
|
+
console.log(chalk.green("\u2713") + ' Created empty profile "my-setup"; bind reviewed instruction sources explicitly.');
|
|
8766
9445
|
}
|
|
8767
9446
|
const machineProfiles = await ensurePlatformProfiles(store);
|
|
8768
9447
|
console.log(chalk.green("\u2713") + ` Ensured ${machineProfiles.length} machine-aware profile(s)`);
|
|
@@ -8773,7 +9452,7 @@ DB stats:`));
|
|
|
8773
9452
|
if (count > 0)
|
|
8774
9453
|
console.log(` ${key.padEnd(18)} ${count}`);
|
|
8775
9454
|
}
|
|
8776
|
-
const location = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] ||
|
|
9455
|
+
const location = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join11(getRawStoreRoot(), "instructions.db");
|
|
8777
9456
|
console.log(chalk.dim(`
|
|
8778
9457
|
${store.mode === "api" ? "API" : "DB"}: ${location}`));
|
|
8779
9458
|
});
|
|
@@ -8874,13 +9553,13 @@ storageCmd.command("status").description("Show redacted S3 configuration status
|
|
|
8874
9553
|
});
|
|
8875
9554
|
var storageBackupCmd = storageCmd.command("backup").description("Push, pull, and verify immutable Instructions backup objects");
|
|
8876
9555
|
storageBackupCmd.command("push <file>").description("Upload an immutable backup archive and integrity manifest").requiredOption("--id <backup-id>", "stable immutable backup identifier").option("--content-type <type>", "payload content type", "application/gzip").option("--dry-run", "plan keys and hashes without contacting S3").option("--json", "output JSON").action(async (file, opts) => {
|
|
8877
|
-
const abs =
|
|
8878
|
-
if (!existsSync11(abs) || !
|
|
9556
|
+
const abs = resolve7(file);
|
|
9557
|
+
if (!existsSync11(abs) || !lstatSync5(abs).isFile())
|
|
8879
9558
|
throw new Error(`Backup file not found: ${abs}`);
|
|
8880
9559
|
const [{ loadInstructionsS3Config }, backup, objectStore] = await Promise.all([
|
|
8881
9560
|
import("../chunks/s3-config-hhght91k.js"),
|
|
8882
|
-
import("../chunks/s3-backup-
|
|
8883
|
-
import("../chunks/s3-object-store-
|
|
9561
|
+
import("../chunks/s3-backup-hxe3zpfz.js"),
|
|
9562
|
+
import("../chunks/s3-object-store-n0s82swt.js")
|
|
8884
9563
|
]);
|
|
8885
9564
|
const config = loadInstructionsS3Config(process.env);
|
|
8886
9565
|
if (!config)
|
|
@@ -8927,8 +9606,8 @@ storageBackupCmd.command("verify <backup-id>").description("Verify the immutable
|
|
|
8927
9606
|
const versions = normalizeBackupVersionOptions(opts);
|
|
8928
9607
|
const [{ loadInstructionsS3Config }, backup, objectStore] = await Promise.all([
|
|
8929
9608
|
import("../chunks/s3-config-hhght91k.js"),
|
|
8930
|
-
import("../chunks/s3-backup-
|
|
8931
|
-
import("../chunks/s3-object-store-
|
|
9609
|
+
import("../chunks/s3-backup-hxe3zpfz.js"),
|
|
9610
|
+
import("../chunks/s3-object-store-n0s82swt.js")
|
|
8932
9611
|
]);
|
|
8933
9612
|
const config = loadInstructionsS3Config(process.env);
|
|
8934
9613
|
if (!config)
|
|
@@ -8953,8 +9632,8 @@ storageBackupCmd.command("pull <backup-id>").description("Download and verify an
|
|
|
8953
9632
|
const versions = normalizeBackupVersionOptions(opts);
|
|
8954
9633
|
const [{ loadInstructionsS3Config }, backup, objectStore] = await Promise.all([
|
|
8955
9634
|
import("../chunks/s3-config-hhght91k.js"),
|
|
8956
|
-
import("../chunks/s3-backup-
|
|
8957
|
-
import("../chunks/s3-object-store-
|
|
9635
|
+
import("../chunks/s3-backup-hxe3zpfz.js"),
|
|
9636
|
+
import("../chunks/s3-object-store-n0s82swt.js")
|
|
8958
9637
|
]);
|
|
8959
9638
|
const config = loadInstructionsS3Config(process.env);
|
|
8960
9639
|
if (!config)
|
|
@@ -8965,16 +9644,16 @@ storageBackupCmd.command("pull <backup-id>").description("Download and verify an
|
|
|
8965
9644
|
backupId,
|
|
8966
9645
|
...versions
|
|
8967
9646
|
});
|
|
8968
|
-
const output =
|
|
9647
|
+
const output = resolve7(opts.output);
|
|
8969
9648
|
if (existsSync11(output)) {
|
|
8970
9649
|
if (!opts.force)
|
|
8971
9650
|
throw new Error(`Refusing to overwrite existing backup file: ${output}`);
|
|
8972
|
-
if (!
|
|
9651
|
+
if (!lstatSync5(output).isFile() || lstatSync5(output).isSymbolicLink()) {
|
|
8973
9652
|
throw new Error(`Refusing to replace a non-regular backup destination: ${output}`);
|
|
8974
9653
|
}
|
|
8975
9654
|
}
|
|
8976
|
-
mkdirSync6(
|
|
8977
|
-
const temp =
|
|
9655
|
+
mkdirSync6(dirname7(output), { recursive: true, mode: 448 });
|
|
9656
|
+
const temp = join11(dirname7(output), `.${basename2(output)}.tmp-${process.pid}-${Date.now()}`);
|
|
8978
9657
|
try {
|
|
8979
9658
|
writeFileSync5(temp, pulled.bytes, { flag: "wx", mode: 384 });
|
|
8980
9659
|
renameSync2(temp, output);
|
|
@@ -8999,10 +9678,10 @@ storageBackupCmd.command("pull <backup-id>").description("Download and verify an
|
|
|
8999
9678
|
});
|
|
9000
9679
|
program.command("backup").description("Export configs to a timestamped backup file").action(async () => {
|
|
9001
9680
|
const { mkdirSync: mk } = await import("fs");
|
|
9002
|
-
const backupDir =
|
|
9681
|
+
const backupDir = join11(getRawStoreRoot(), "backups");
|
|
9003
9682
|
mk(backupDir, { recursive: true });
|
|
9004
9683
|
const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 19);
|
|
9005
|
-
const outPath =
|
|
9684
|
+
const outPath = join11(backupDir, `configs-${ts}.tar.gz`);
|
|
9006
9685
|
const result = await exportConfigs(outPath, { store: resolveConfigStore() });
|
|
9007
9686
|
const { statSync: st } = await import("fs");
|
|
9008
9687
|
const size = st(outPath).size;
|
|
@@ -9176,7 +9855,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9176
9855
|
const store = resolveConfigStore();
|
|
9177
9856
|
const interval = Number(opts.interval);
|
|
9178
9857
|
const { statSync: st } = await import("fs");
|
|
9179
|
-
const { expandPath: expandPath2 } = await import("../chunks/apply-
|
|
9858
|
+
const { expandPath: expandPath2 } = await import("../chunks/apply-wj7wh5km.js");
|
|
9180
9859
|
console.log(chalk.bold("@hasna/instructions watch") + chalk.dim(` \u2014 polling every ${interval}ms`));
|
|
9181
9860
|
console.log(chalk.dim(`Watching known config files for changes\u2026
|
|
9182
9861
|
`));
|
|
@@ -9188,7 +9867,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9188
9867
|
continue;
|
|
9189
9868
|
const { readdirSync: readdirSync3 } = await import("fs");
|
|
9190
9869
|
for (const f of readdirSync3(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
9191
|
-
const abs =
|
|
9870
|
+
const abs = join11(absDir, f);
|
|
9192
9871
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
9193
9872
|
}
|
|
9194
9873
|
} else {
|
|
@@ -9216,7 +9895,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9216
9895
|
if (!existsSync11(absDir))
|
|
9217
9896
|
continue;
|
|
9218
9897
|
for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
9219
|
-
const abs =
|
|
9898
|
+
const abs = join11(absDir, f);
|
|
9220
9899
|
if (!mtimes.has(abs)) {
|
|
9221
9900
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
9222
9901
|
changed++;
|
|
@@ -9256,7 +9935,7 @@ program.command("report").description("Summary of stored configs, drift, and eco
|
|
|
9256
9935
|
missing++;
|
|
9257
9936
|
continue;
|
|
9258
9937
|
}
|
|
9259
|
-
const disk =
|
|
9938
|
+
const disk = readFileSync8(abs, "utf-8");
|
|
9260
9939
|
const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(c.target_path, c.format));
|
|
9261
9940
|
if (redactedDisk !== c.content)
|
|
9262
9941
|
drifted++;
|