@haystackeditor/cli 0.25.1 ā 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -463
- package/dist/capture/app-config.js +30 -6
- package/dist/commands/capture-brief.js +45 -35
- package/dist/commands/capture-contract.js +4 -4
- package/dist/commands/crawl-report.js +196 -0
- package/dist/commands/db-profile-upload.js +3 -3
- package/dist/commands/feedback.js +66 -0
- package/dist/commands/init-telemetry.js +55 -9
- package/dist/commands/init.js +8 -3
- package/dist/commands/lockfile-pin.js +307 -0
- package/dist/commands/telemetry-token.js +3 -3
- package/dist/commands/tokens.js +4 -4
- package/dist/commands/verify-explore.js +3 -3
- package/dist/commands/verify-history.js +2 -2
- package/dist/commands/verify-onboarding.js +13 -16
- package/dist/commands/verify-precompute.js +3 -4
- package/dist/commands/verify.js +31 -118
- package/dist/index.js +55 -968
- package/dist/schema.js +4 -10
- package/dist/utils/haystack-api.js +8 -36
- package/package.json +1 -5
- package/schemas/feedback.v1.json +13 -0
- package/schemas/pre-verify.v2.json +240 -0
- package/schemas/verify-raw.v1.json +1132 -0
- package/schemas/verify.v2.json +655 -0
- package/dist/assets/hooks/agent-context/detect.ts +0 -316
- package/dist/assets/hooks/agent-context/format.ts +0 -100
- package/dist/assets/hooks/agent-context/index.ts +0 -41
- package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
- package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
- package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
- package/dist/assets/hooks/agent-context/types.ts +0 -58
- package/dist/assets/hooks/llm-rules-template.md +0 -59
- package/dist/assets/hooks/package-lock.json +0 -598
- package/dist/assets/hooks/package.json +0 -12
- package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
- package/dist/assets/hooks/scripts/post-commit.sh +0 -5
- package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
- package/dist/assets/hooks/scripts/pre-push.sh +0 -25
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
- package/dist/assets/hooks/truncation-checker/index.ts +0 -595
- package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
- package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
- package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
- package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
- package/dist/assets/skills/map-your-system.md +0 -143
- package/dist/assets/skills/submit.md +0 -200
- package/dist/commands/ask.js +0 -20
- package/dist/commands/cloud-verifier-behaviors.js +0 -218
- package/dist/commands/cloud-verifier-data-store-census.js +0 -539
- package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
- package/dist/commands/cloud-verifier-identity-census.js +0 -4060
- package/dist/commands/cloud-verifier-materialization.js +0 -704
- package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
- package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
- package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
- package/dist/commands/cloud-verifier-universe.js +0 -10178
- package/dist/commands/config.js +0 -549
- package/dist/commands/design-verify.js +0 -311
- package/dist/commands/dismiss.js +0 -159
- package/dist/commands/hooks.js +0 -226
- package/dist/commands/inbox.js +0 -137
- package/dist/commands/mcp.js +0 -201
- package/dist/commands/policy.js +0 -371
- package/dist/commands/pr-status.js +0 -207
- package/dist/commands/pr.js +0 -105
- package/dist/commands/prepare-universe-review.js +0 -1092
- package/dist/commands/production-source-deny-policy.js +0 -100
- package/dist/commands/request-review.js +0 -74
- package/dist/commands/review.js +0 -191
- package/dist/commands/rules.js +0 -98
- package/dist/commands/scaffold-provisional-universe.js +0 -806
- package/dist/commands/setup.js +0 -1170
- package/dist/commands/skills.js +0 -447
- package/dist/commands/status.js +0 -35
- package/dist/commands/submit.js +0 -745
- package/dist/commands/system-map.js +0 -228
- package/dist/commands/triage.js +0 -598
- package/dist/commands/webhooks.js +0 -241
- package/dist/states.js +0 -46
- package/dist/tools/detect.js +0 -832
- package/dist/triage/astra.js +0 -202
- package/dist/triage/prompts.js +0 -188
- package/dist/triage/runner.js +0 -200
- package/dist/triage/types.js +0 -7
- package/dist/types.js +0 -326
- package/dist/utils/action-output.js +0 -26
- package/dist/utils/analysis-api.js +0 -416
- package/dist/utils/config.js +0 -54
- package/dist/utils/design-verifier-api.js +0 -294
- package/dist/utils/design-verifier-history.js +0 -79
- package/dist/utils/design-verifier-result.js +0 -424
- package/dist/utils/github-api.js +0 -324
- package/dist/utils/pending-state.js +0 -86
- package/dist/utils/pr-ref.js +0 -56
- package/dist/utils/prompter.js +0 -328
- package/schemas/action.v1.json +0 -22
- package/schemas/ask.v1.json +0 -40
- package/schemas/inbox.v1.json +0 -27
- package/schemas/pr-status.v1.json +0 -61
- package/schemas/pr.v1.json +0 -97
- package/schemas/pr.v3.json +0 -45
- package/schemas/setup.v1.json +0 -75
- package/schemas/submit.v1.json +0 -90
- package/schemas/triage.v1.json +0 -103
- package/schemas/triage.v2.json +0 -64
package/dist/commands/skills.js
DELETED
|
@@ -1,447 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skills command - installs Haystack-owned skill files while preserving any
|
|
3
|
-
* customer-owned skills that share the discovery directories.
|
|
4
|
-
*/
|
|
5
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
-
import { chmodSync, copyFileSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync, } from 'node:fs';
|
|
7
|
-
import { dirname, isAbsolute, join, posix, relative, sep } from 'node:path';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import chalk from 'chalk';
|
|
10
|
-
import { findGitRoot } from '../utils/hooks.js';
|
|
11
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const MANIFEST_NAME = '.haystack-managed.json';
|
|
13
|
-
const MANIFEST_SCHEMA_VERSION = 1;
|
|
14
|
-
const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
15
|
-
/** Directory containing bundled skill files. */
|
|
16
|
-
function getSkillsSourceDir() {
|
|
17
|
-
// In dist/commands/skills.js, assets are at dist/assets/skills/.
|
|
18
|
-
return join(__dirname, '..', 'assets', 'skills');
|
|
19
|
-
}
|
|
20
|
-
const SKILL_FILES = [
|
|
21
|
-
'submit.md',
|
|
22
|
-
'map-your-system.md',
|
|
23
|
-
];
|
|
24
|
-
const SKILL_DIRECTORIES = [
|
|
25
|
-
'map-cloud-verifier-universe',
|
|
26
|
-
];
|
|
27
|
-
// The manifest-free installer predates every managed directory payload. Only
|
|
28
|
-
// these fixed files can be classified as legacy Haystack content without an
|
|
29
|
-
// ownership receipt; all other existing payload paths are user-owned.
|
|
30
|
-
const LEGACY_MANAGED_FILE_PATHS = new Set([
|
|
31
|
-
'setup-haystack.md',
|
|
32
|
-
'submit.md',
|
|
33
|
-
'map-your-system.md',
|
|
34
|
-
]);
|
|
35
|
-
// Keep historical roots here after removing a bundled asset. They are the
|
|
36
|
-
// only namespaces an earlier valid manifest may authorize us to clean up.
|
|
37
|
-
const MANAGED_FILE_ROOTS = new Set(['setup-haystack.md', ...SKILL_FILES]);
|
|
38
|
-
const MANAGED_DIRECTORY_ROOTS = new Set(SKILL_DIRECTORIES);
|
|
39
|
-
const CLAUDE_MANAGED_FILE_ROOTS = new Set([
|
|
40
|
-
'setup-haystack.md',
|
|
41
|
-
'map-your-system.md',
|
|
42
|
-
'map-cloud-verifier-universe.md',
|
|
43
|
-
]);
|
|
44
|
-
const CLAUDE_COMMANDS = new Map([
|
|
45
|
-
[
|
|
46
|
-
'map-your-system.md',
|
|
47
|
-
'# Map Your System for Haystack QA\n\nFollow .agents/skills/map-your-system.md to create .haystack/system.yml ā verified facts about how this system builds, boots, seeds data, and logs in, so Haystack QA can run it.\n',
|
|
48
|
-
],
|
|
49
|
-
[
|
|
50
|
-
'map-cloud-verifier-universe.md',
|
|
51
|
-
'# Map a Cloud Verifier Universe\n\nFollow .agents/skills/map-cloud-verifier-universe/SKILL.md to map this application into a production-derived hermetic universe for on-demand Cloud Verifier.\n',
|
|
52
|
-
],
|
|
53
|
-
]);
|
|
54
|
-
function isRecord(value) {
|
|
55
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
56
|
-
}
|
|
57
|
-
function entryExists(path) {
|
|
58
|
-
try {
|
|
59
|
-
lstatSync(path);
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
if (error.code === 'ENOENT')
|
|
64
|
-
return false;
|
|
65
|
-
throw error;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function hasExactKeys(record, expected) {
|
|
69
|
-
const actual = Object.keys(record).sort();
|
|
70
|
-
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
71
|
-
}
|
|
72
|
-
function sha256(contents) {
|
|
73
|
-
return `sha256:${createHash('sha256').update(contents).digest('hex')}`;
|
|
74
|
-
}
|
|
75
|
-
function manifestDigest(files) {
|
|
76
|
-
return sha256(JSON.stringify(files.map(({ path, sha256: digest }) => ({ path, sha256: digest }))));
|
|
77
|
-
}
|
|
78
|
-
function compareText(a, b) {
|
|
79
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
80
|
-
}
|
|
81
|
-
function inside(root, candidate) {
|
|
82
|
-
const rel = relative(root, candidate);
|
|
83
|
-
return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`));
|
|
84
|
-
}
|
|
85
|
-
function validateManagedPath(path) {
|
|
86
|
-
if (typeof path !== 'string' || path.length === 0 || path.includes('\\') || path.includes('\0')) {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
if (isAbsolute(path) || posix.normalize(path) !== path)
|
|
90
|
-
return false;
|
|
91
|
-
const components = path.split('/');
|
|
92
|
-
if (components.some((component) => component === '' || component === '.' || component === '..')) {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
const [root] = components;
|
|
96
|
-
return ((components.length === 1 && MANAGED_FILE_ROOTS.has(root)) ||
|
|
97
|
-
(components.length > 1 && MANAGED_DIRECTORY_ROOTS.has(root)));
|
|
98
|
-
}
|
|
99
|
-
function destinationPath(targetDir, managedPath) {
|
|
100
|
-
return join(targetDir, ...managedPath.split('/'));
|
|
101
|
-
}
|
|
102
|
-
function assertExistingDirectoryInside(root, path) {
|
|
103
|
-
const stat = lstatSync(path);
|
|
104
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
105
|
-
throw new Error(`Refusing skills install: destination ancestor is not a real directory: ${path}`);
|
|
106
|
-
}
|
|
107
|
-
const real = realpathSync(path);
|
|
108
|
-
if (!inside(root, real)) {
|
|
109
|
-
throw new Error(`Refusing skills install: destination escapes the repository: ${path}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
/** Check every existing destination component without changing the repository. */
|
|
113
|
-
function preflightDestination(root, relativeTarget) {
|
|
114
|
-
let current = root;
|
|
115
|
-
for (const component of relativeTarget) {
|
|
116
|
-
current = join(current, component);
|
|
117
|
-
if (!entryExists(current))
|
|
118
|
-
return;
|
|
119
|
-
assertExistingDirectoryInside(root, current);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
/** Create one destination component at a time, rechecking each one for symlinks. */
|
|
123
|
-
function ensureSafeDirectory(root, relativeTarget) {
|
|
124
|
-
let current = root;
|
|
125
|
-
for (const component of relativeTarget) {
|
|
126
|
-
current = join(current, component);
|
|
127
|
-
if (!entryExists(current))
|
|
128
|
-
mkdirSync(current, { mode: 0o755 });
|
|
129
|
-
assertExistingDirectoryInside(root, current);
|
|
130
|
-
}
|
|
131
|
-
return current;
|
|
132
|
-
}
|
|
133
|
-
function assertManagedLeafSafe(root, targetDir, managedPath) {
|
|
134
|
-
let current = targetDir;
|
|
135
|
-
const components = managedPath.split('/');
|
|
136
|
-
components.forEach((component, index) => {
|
|
137
|
-
current = join(current, component);
|
|
138
|
-
if (!entryExists(current))
|
|
139
|
-
return;
|
|
140
|
-
const stat = lstatSync(current);
|
|
141
|
-
if (stat.isSymbolicLink()) {
|
|
142
|
-
throw new Error(`Refusing skills install: managed path contains a symlink: ${managedPath}`);
|
|
143
|
-
}
|
|
144
|
-
if (index < components.length - 1 && !stat.isDirectory()) {
|
|
145
|
-
throw new Error(`Refusing skills install: managed path ancestor is not a directory: ${managedPath}`);
|
|
146
|
-
}
|
|
147
|
-
if (index === components.length - 1 && !stat.isFile()) {
|
|
148
|
-
throw new Error(`Refusing skills install: managed file path is not a regular file: ${managedPath}`);
|
|
149
|
-
}
|
|
150
|
-
const real = realpathSync(current);
|
|
151
|
-
if (!inside(root, real)) {
|
|
152
|
-
throw new Error(`Refusing skills install: managed path escapes the repository: ${managedPath}`);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
function collectSourceDirectory(sourceDir, relativePrefix, payload) {
|
|
157
|
-
const sourceStat = lstatSync(sourceDir);
|
|
158
|
-
if (sourceStat.isSymbolicLink() || !sourceStat.isDirectory()) {
|
|
159
|
-
throw new Error(`Bundled skill directory is not a real directory: ${sourceDir}`);
|
|
160
|
-
}
|
|
161
|
-
for (const entry of readdirSync(sourceDir, { withFileTypes: true }).sort((a, b) => compareText(a.name, b.name))) {
|
|
162
|
-
const source = join(sourceDir, entry.name);
|
|
163
|
-
const managedPath = posix.join(relativePrefix, entry.name);
|
|
164
|
-
const stat = lstatSync(source);
|
|
165
|
-
if (stat.isSymbolicLink()) {
|
|
166
|
-
throw new Error(`Bundled skills may not contain symlinks: ${source}`);
|
|
167
|
-
}
|
|
168
|
-
if (stat.isDirectory()) {
|
|
169
|
-
collectSourceDirectory(source, managedPath, payload);
|
|
170
|
-
}
|
|
171
|
-
else if (stat.isFile()) {
|
|
172
|
-
const contents = readFileSync(source);
|
|
173
|
-
payload.set(managedPath, {
|
|
174
|
-
path: managedPath,
|
|
175
|
-
sha256: sha256(contents),
|
|
176
|
-
contents,
|
|
177
|
-
mode: stat.mode & 0o777,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
throw new Error(`Bundled skills may contain only regular files and directories: ${source}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
function bundledSkillsPayload() {
|
|
186
|
-
const sourceDir = getSkillsSourceDir();
|
|
187
|
-
if (!entryExists(sourceDir)) {
|
|
188
|
-
throw new Error(`Skills source directory not found: ${sourceDir}`);
|
|
189
|
-
}
|
|
190
|
-
const payload = new Map();
|
|
191
|
-
for (const file of SKILL_FILES) {
|
|
192
|
-
const source = join(sourceDir, file);
|
|
193
|
-
if (!entryExists(source))
|
|
194
|
-
continue;
|
|
195
|
-
const stat = lstatSync(source);
|
|
196
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
197
|
-
throw new Error(`Bundled skill file is not a regular file: ${source}`);
|
|
198
|
-
}
|
|
199
|
-
const contents = readFileSync(source);
|
|
200
|
-
payload.set(file, {
|
|
201
|
-
path: file,
|
|
202
|
-
sha256: sha256(contents),
|
|
203
|
-
contents,
|
|
204
|
-
mode: stat.mode & 0o777,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
for (const directory of SKILL_DIRECTORIES) {
|
|
208
|
-
const source = join(sourceDir, directory);
|
|
209
|
-
if (!entryExists(source))
|
|
210
|
-
continue;
|
|
211
|
-
collectSourceDirectory(source, directory, payload);
|
|
212
|
-
}
|
|
213
|
-
if (payload.size === 0)
|
|
214
|
-
throw new Error(`No bundled skills found in ${sourceDir}`);
|
|
215
|
-
return payload;
|
|
216
|
-
}
|
|
217
|
-
function textPayload(files) {
|
|
218
|
-
return new Map([...files.entries()].map(([path, text]) => {
|
|
219
|
-
const contents = Buffer.from(text, 'utf8');
|
|
220
|
-
return [path, { path, sha256: sha256(contents), contents, mode: 0o644 }];
|
|
221
|
-
}));
|
|
222
|
-
}
|
|
223
|
-
function parseManifest(targetDir, allowedPath) {
|
|
224
|
-
const path = join(targetDir, MANIFEST_NAME);
|
|
225
|
-
if (!entryExists(path))
|
|
226
|
-
return null;
|
|
227
|
-
const stat = lstatSync(path);
|
|
228
|
-
if (stat.isSymbolicLink() || !stat.isFile() || stat.size > 1024 * 1024) {
|
|
229
|
-
throw new Error(`Refusing skills install: managed manifest is not a trusted regular file: ${path}`);
|
|
230
|
-
}
|
|
231
|
-
let parsed;
|
|
232
|
-
try {
|
|
233
|
-
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
234
|
-
}
|
|
235
|
-
catch {
|
|
236
|
-
throw new Error(`Refusing skills install: managed manifest is malformed: ${path}`);
|
|
237
|
-
}
|
|
238
|
-
if (!isRecord(parsed) ||
|
|
239
|
-
!hasExactKeys(parsed, ['bundle_digest', 'files', 'schema_version']) ||
|
|
240
|
-
parsed.schema_version !== MANIFEST_SCHEMA_VERSION ||
|
|
241
|
-
typeof parsed.bundle_digest !== 'string' ||
|
|
242
|
-
!SHA256_PATTERN.test(parsed.bundle_digest) ||
|
|
243
|
-
!Array.isArray(parsed.files)) {
|
|
244
|
-
throw new Error(`Refusing skills install: managed manifest has an unsupported schema: ${path}`);
|
|
245
|
-
}
|
|
246
|
-
const files = [];
|
|
247
|
-
for (const value of parsed.files) {
|
|
248
|
-
if (!isRecord(value) ||
|
|
249
|
-
!hasExactKeys(value, ['path', 'sha256']) ||
|
|
250
|
-
!allowedPath(value.path) ||
|
|
251
|
-
typeof value.sha256 !== 'string' ||
|
|
252
|
-
!SHA256_PATTERN.test(value.sha256)) {
|
|
253
|
-
throw new Error(`Refusing skills install: managed manifest contains an unsafe entry: ${path}`);
|
|
254
|
-
}
|
|
255
|
-
files.push({ path: value.path, sha256: value.sha256 });
|
|
256
|
-
}
|
|
257
|
-
const sorted = [...files].sort((a, b) => compareText(a.path, b.path));
|
|
258
|
-
if (files.some((file, index) => file.path !== sorted[index]?.path) ||
|
|
259
|
-
files.some((file, index) => index > 0 && file.path === files[index - 1]?.path) ||
|
|
260
|
-
manifestDigest(files) !== parsed.bundle_digest) {
|
|
261
|
-
throw new Error(`Refusing skills install: managed manifest is inconsistent: ${path}`);
|
|
262
|
-
}
|
|
263
|
-
return {
|
|
264
|
-
schema_version: MANIFEST_SCHEMA_VERSION,
|
|
265
|
-
bundle_digest: parsed.bundle_digest,
|
|
266
|
-
files,
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
function copyTreeWithoutFollowingSymlinks(sourceDir, destinationDir) {
|
|
270
|
-
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
271
|
-
const source = join(sourceDir, entry.name);
|
|
272
|
-
const destination = join(destinationDir, entry.name);
|
|
273
|
-
const stat = lstatSync(source);
|
|
274
|
-
if (stat.isSymbolicLink()) {
|
|
275
|
-
symlinkSync(readlinkSync(source), destination);
|
|
276
|
-
}
|
|
277
|
-
else if (stat.isDirectory()) {
|
|
278
|
-
mkdirSync(destination, { mode: stat.mode & 0o777 });
|
|
279
|
-
copyTreeWithoutFollowingSymlinks(source, destination);
|
|
280
|
-
}
|
|
281
|
-
else if (stat.isFile()) {
|
|
282
|
-
copyFileSync(source, destination);
|
|
283
|
-
chmodSync(destination, stat.mode & 0o777);
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
throw new Error(`Refusing skills install: destination contains an unsupported file type: ${source}`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
function writePayloadToStage(stageDir, payload, previouslyManaged, manifest) {
|
|
291
|
-
for (const managedPath of previouslyManaged) {
|
|
292
|
-
const stagedPath = destinationPath(stageDir, managedPath);
|
|
293
|
-
if (entryExists(stagedPath))
|
|
294
|
-
rmSync(stagedPath, { force: true });
|
|
295
|
-
}
|
|
296
|
-
for (const file of payload.values()) {
|
|
297
|
-
const destination = destinationPath(stageDir, file.path);
|
|
298
|
-
const parent = dirname(destination);
|
|
299
|
-
if (!entryExists(parent))
|
|
300
|
-
mkdirSync(parent, { recursive: true, mode: 0o755 });
|
|
301
|
-
if (entryExists(destination)) {
|
|
302
|
-
throw new Error(`Refusing skills install: new managed file collides with a user file: ${file.path}`);
|
|
303
|
-
}
|
|
304
|
-
writeFileSync(destination, file.contents, { mode: file.mode });
|
|
305
|
-
}
|
|
306
|
-
writeFileSync(join(stageDir, MANIFEST_NAME), `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
307
|
-
mode: 0o644,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
function replaceDirectoryFromStage(targetDir, stageDir) {
|
|
311
|
-
const parent = dirname(targetDir);
|
|
312
|
-
const backupDir = join(parent, `.haystack-${posix.basename(targetDir)}-backup-${randomUUID()}`);
|
|
313
|
-
const targetExisted = entryExists(targetDir);
|
|
314
|
-
let oldMoved = false;
|
|
315
|
-
let newMoved = false;
|
|
316
|
-
try {
|
|
317
|
-
if (targetExisted) {
|
|
318
|
-
renameSync(targetDir, backupDir);
|
|
319
|
-
oldMoved = true;
|
|
320
|
-
}
|
|
321
|
-
renameSync(stageDir, targetDir);
|
|
322
|
-
newMoved = true;
|
|
323
|
-
if (oldMoved)
|
|
324
|
-
rmSync(backupDir, { recursive: true });
|
|
325
|
-
}
|
|
326
|
-
catch (error) {
|
|
327
|
-
try {
|
|
328
|
-
if (newMoved && entryExists(targetDir))
|
|
329
|
-
renameSync(targetDir, stageDir);
|
|
330
|
-
if (oldMoved && entryExists(backupDir) && !entryExists(targetDir)) {
|
|
331
|
-
renameSync(backupDir, targetDir);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
finally {
|
|
335
|
-
if (entryExists(stageDir))
|
|
336
|
-
rmSync(stageDir, { recursive: true, force: true });
|
|
337
|
-
}
|
|
338
|
-
throw error;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
function installManagedPayload(root, relativeTarget, payload, allowedPath) {
|
|
342
|
-
const parent = ensureSafeDirectory(root, relativeTarget.slice(0, -1));
|
|
343
|
-
const targetDir = join(parent, relativeTarget[relativeTarget.length - 1]);
|
|
344
|
-
if (entryExists(targetDir))
|
|
345
|
-
assertExistingDirectoryInside(root, targetDir);
|
|
346
|
-
const previous = entryExists(targetDir) ? parseManifest(targetDir, allowedPath) : null;
|
|
347
|
-
const previouslyManaged = new Set(previous?.files.map((file) => file.path) ?? []);
|
|
348
|
-
if (previous) {
|
|
349
|
-
for (const file of previous.files) {
|
|
350
|
-
assertManagedLeafSafe(root, targetDir, file.path);
|
|
351
|
-
const existing = destinationPath(targetDir, file.path);
|
|
352
|
-
if (entryExists(existing) && sha256(readFileSync(existing)) !== file.sha256) {
|
|
353
|
-
throw new Error(`Refusing skills install: managed file was modified after installation: ${file.path}`);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
for (const file of payload.values()) {
|
|
358
|
-
assertManagedLeafSafe(root, targetDir, file.path);
|
|
359
|
-
const existing = destinationPath(targetDir, file.path);
|
|
360
|
-
if (!entryExists(existing))
|
|
361
|
-
continue;
|
|
362
|
-
if ((previous && !previouslyManaged.has(file.path)) ||
|
|
363
|
-
(!previous && !LEGACY_MANAGED_FILE_PATHS.has(file.path))) {
|
|
364
|
-
throw new Error(`Refusing skills install: new managed file collides with a user file: ${file.path}`);
|
|
365
|
-
}
|
|
366
|
-
// One-time migration from the legacy installer: only its three historical
|
|
367
|
-
// fixed files may be adopted when no ownership manifest was written.
|
|
368
|
-
previouslyManaged.add(file.path);
|
|
369
|
-
}
|
|
370
|
-
const files = [...payload.values()]
|
|
371
|
-
.map(({ path, sha256: digest }) => ({ path, sha256: digest }))
|
|
372
|
-
.sort((a, b) => compareText(a.path, b.path));
|
|
373
|
-
const nextManifest = {
|
|
374
|
-
schema_version: MANIFEST_SCHEMA_VERSION,
|
|
375
|
-
bundle_digest: manifestDigest(files),
|
|
376
|
-
files,
|
|
377
|
-
};
|
|
378
|
-
const targetMode = entryExists(targetDir) ? lstatSync(targetDir).mode & 0o777 : 0o755;
|
|
379
|
-
const stageDir = mkdtempSync(join(parent, `.haystack-${relativeTarget.at(-1)}-stage-`));
|
|
380
|
-
try {
|
|
381
|
-
if (entryExists(targetDir))
|
|
382
|
-
copyTreeWithoutFollowingSymlinks(targetDir, stageDir);
|
|
383
|
-
writePayloadToStage(stageDir, payload, previouslyManaged, nextManifest);
|
|
384
|
-
chmodSync(stageDir, targetMode);
|
|
385
|
-
replaceDirectoryFromStage(targetDir, stageDir);
|
|
386
|
-
}
|
|
387
|
-
catch (error) {
|
|
388
|
-
if (entryExists(stageDir))
|
|
389
|
-
rmSync(stageDir, { recursive: true, force: true });
|
|
390
|
-
throw error;
|
|
391
|
-
}
|
|
392
|
-
return nextManifest;
|
|
393
|
-
}
|
|
394
|
-
function parseCli(value) {
|
|
395
|
-
if (value === undefined)
|
|
396
|
-
return undefined;
|
|
397
|
-
if (value === 'claude' || value === 'codex' || value === 'cursor' || value === 'manual') {
|
|
398
|
-
return value;
|
|
399
|
-
}
|
|
400
|
-
throw new Error(`Unsupported coding CLI "${value}". Use claude, codex, cursor, or manual.`);
|
|
401
|
-
}
|
|
402
|
-
function claudeManagedPath(path) {
|
|
403
|
-
return typeof path === 'string' && CLAUDE_MANAGED_FILE_ROOTS.has(path);
|
|
404
|
-
}
|
|
405
|
-
export async function installSkills(options) {
|
|
406
|
-
console.log(chalk.cyan('\nš¦ Installing Haystack skills...\n'));
|
|
407
|
-
const cli = parseCli(options.cli);
|
|
408
|
-
const gitRoot = findGitRoot();
|
|
409
|
-
if (!gitRoot) {
|
|
410
|
-
throw new Error('Skills must be installed from inside a Git repository.');
|
|
411
|
-
}
|
|
412
|
-
const projectRoot = realpathSync(gitRoot);
|
|
413
|
-
// Preflight all selected destinations before writing either one.
|
|
414
|
-
preflightDestination(projectRoot, ['.agents', 'skills']);
|
|
415
|
-
if (cli === 'claude')
|
|
416
|
-
preflightDestination(projectRoot, ['.claude', 'commands']);
|
|
417
|
-
const agentsSkillsDir = join(projectRoot, '.agents', 'skills');
|
|
418
|
-
console.log(chalk.gray(`Copying portable skills to ${agentsSkillsDir}\n`));
|
|
419
|
-
const skillsManifest = installManagedPayload(projectRoot, ['.agents', 'skills'], bundledSkillsPayload(), validateManagedPath);
|
|
420
|
-
if (cli === 'claude') {
|
|
421
|
-
installManagedPayload(projectRoot, ['.claude', 'commands'], textPayload(CLAUDE_COMMANDS), claudeManagedPath);
|
|
422
|
-
}
|
|
423
|
-
showSuccessMessage(cli, projectRoot, skillsManifest.bundle_digest);
|
|
424
|
-
}
|
|
425
|
-
function showSuccessMessage(cli, projectRoot, digest) {
|
|
426
|
-
console.log(chalk.green('\nā
Haystack skills installed!\n'));
|
|
427
|
-
console.log(chalk.gray(`Repository: ${projectRoot}`));
|
|
428
|
-
console.log(chalk.gray(`Bundle: ${digest}`));
|
|
429
|
-
console.log(chalk.white('\nAvailable skills:'));
|
|
430
|
-
console.log(chalk.cyan(' /submit ') + chalk.gray('- Submit a PR via Haystack'));
|
|
431
|
-
console.log(chalk.cyan(' /map-your-system ') + chalk.gray('- Write .haystack/system.yml so Haystack QA can run this system'));
|
|
432
|
-
console.log(chalk.cyan(' /map-cloud-verifier-universe') + chalk.gray(' - Map a production-derived hermetic Cloud Verifier universe'));
|
|
433
|
-
if (cli === 'claude') {
|
|
434
|
-
console.log(chalk.gray('\nClaude Code command shims were installed in .claude/commands.'));
|
|
435
|
-
}
|
|
436
|
-
else if (cli === 'manual') {
|
|
437
|
-
console.log(chalk.gray('\nPoint your coding agent at .agents/skills for portable skill discovery.'));
|
|
438
|
-
}
|
|
439
|
-
console.log();
|
|
440
|
-
}
|
|
441
|
-
export async function listSkills() {
|
|
442
|
-
console.log(chalk.white('\nAvailable Haystack skills:\n'));
|
|
443
|
-
console.log(chalk.cyan(' /submit ') + chalk.gray('- Submit a PR via Haystack'));
|
|
444
|
-
console.log(chalk.cyan(' /map-your-system ') + chalk.gray('- Write .haystack/system.yml so Haystack QA can run this system'));
|
|
445
|
-
console.log(chalk.cyan(' /map-cloud-verifier-universe') + chalk.gray(' - Map a production-derived hermetic Cloud Verifier universe'));
|
|
446
|
-
console.log();
|
|
447
|
-
}
|
package/dist/commands/status.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* haystack status - Check if .haystack.json exists and is valid
|
|
3
|
-
*/
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { loadConfig, findConfigPath } from '../utils/config.js';
|
|
6
|
-
export async function statusCommand() {
|
|
7
|
-
const configPath = await findConfigPath();
|
|
8
|
-
if (!configPath) {
|
|
9
|
-
console.log(chalk.yellow('ā No .haystack.json found'));
|
|
10
|
-
console.log(chalk.dim('\nRun `haystack init` to create one.\n'));
|
|
11
|
-
process.exit(1);
|
|
12
|
-
}
|
|
13
|
-
console.log(chalk.green(`ā Found ${configPath}`));
|
|
14
|
-
try {
|
|
15
|
-
const config = await loadConfig(configPath);
|
|
16
|
-
if (!config) {
|
|
17
|
-
console.log(chalk.red('ā Failed to load config'));
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
console.log(chalk.green('ā Config is valid\n'));
|
|
21
|
-
// Show config summary
|
|
22
|
-
if (config.name) {
|
|
23
|
-
console.log(` ${chalk.dim('Name:')} ${config.name}`);
|
|
24
|
-
}
|
|
25
|
-
if (config.services) {
|
|
26
|
-
const serviceNames = Object.keys(config.services);
|
|
27
|
-
console.log(` ${chalk.dim('Services:')} ${serviceNames.join(', ')}`);
|
|
28
|
-
}
|
|
29
|
-
console.log('');
|
|
30
|
-
}
|
|
31
|
-
catch (e) {
|
|
32
|
-
console.log(chalk.red(`ā Config is invalid: ${e.message}`));
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
}
|