svharness 0.8.0 → 0.11.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 +267 -31
- package/dist/adapters/claude-code.js +1 -0
- package/dist/adapters/codechat.js +1 -0
- package/dist/adapters/cursor.js +1 -0
- package/dist/adapters/generic.js +1 -0
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/opencode.js +17 -0
- package/dist/adapters/qoder.js +1 -0
- package/dist/commands/apply.js +48 -3
- package/dist/commands/convert.js +369 -0
- package/dist/commands/init.js +125 -8
- package/dist/commands/references-apply-skills.js +47 -0
- package/dist/commands/wizard.js +441 -0
- package/dist/config/constants.js +7 -0
- package/dist/config/index.js +18 -0
- package/dist/config/load-config.js +54 -0
- package/dist/config/merge-options.js +115 -0
- package/dist/config/normalize.js +165 -0
- package/dist/config/save-config.js +40 -0
- package/dist/config/types.js +2 -0
- package/dist/core/agent-injector.js +58 -9
- package/dist/core/apply-project-entry.js +92 -0
- package/dist/core/build-project-entry.js +98 -0
- package/dist/core/doc-intake-paths.js +155 -0
- package/dist/core/extra-assets-intake.js +254 -0
- package/dist/core/markitdown-client.js +156 -0
- package/dist/core/next-steps.js +33 -22
- package/dist/core/reference-apply-skills.js +35 -0
- package/dist/core/render-meta.js +2 -1
- package/dist/core/repomix-pack.js +3 -3
- package/dist/core/state.js +44 -24
- package/dist/index.js +207 -132
- package/dist/utils/validate-args.js +100 -0
- package/dist/wiki/wikiTasksWriter.js +5 -6
- package/package.json +2 -1
- package/templates/_shared/apply-skills/harness-apply-skills-main.md +45 -14
- package/templates/_shared/build-rules/harness-build-rule-agent-agnostic.md +5 -5
- package/templates/_shared/build-rules/harness-build-rule-chinese-only.md +5 -5
- package/templates/_shared/build-rules/harness-build-rule-convert-check.md +46 -0
- package/templates/_shared/build-rules/harness-build-rule-memory-write.md +1 -1
- package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +36 -10
- package/templates/_shared/build-rules/harness-build-rule-skills-tasks-output.md +3 -2
- package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +3 -3
- package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +36 -16
- package/templates/_shared/build-skills/harness-build-skill-agent-env-merge.md +75 -0
- package/templates/_shared/build-skills/harness-build-skill-knowledge-builder.md +49 -85
- package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +35 -18
- package/templates/_shared/build-skills/harness-build-skill-references-intake.md +91 -0
- package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +19 -9
- package/templates/_shared/build-skills/harness-build-skill-wiki-writer.md +24 -24
- package/templates/_shared/build-skills/harness-build-skills-main.md +83 -0
- package/templates/_shared/meta/AGENTS_APPLY.md.ejs +139 -0
- package/templates/_shared/meta/{AGENTS.md.ejs → AGENTS_BUILD.md.ejs} +7 -5
- package/templates/_shared/meta/CHANGELOG.md.ejs +3 -3
- package/templates/_shared/meta/README.md.ejs +11 -9
- package/templates/_shared/meta/harness.yaml.ejs +28 -7
- package/templates/_shared/skeleton/baseline/code/.gitkeep +1 -0
- package/templates/_shared/skeleton/baseline/wiki/.gitkeep +1 -0
- package/templates/_shared/skeleton/references/apply-skills-registry.example.yaml +11 -0
- package/templates/_shared/skeleton/references/md/.gitkeep +1 -0
- package/templates/_shared/skeleton/references/raw/.gitkeep +1 -0
- package/templates/_shared/skeleton/references/yaml/.gitkeep +1 -0
- package/templates/_shared/skeleton/requirements/md/.gitkeep +1 -0
- package/templates/_shared/skeleton/requirements/raw/.gitkeep +1 -0
- package/templates/_shared/skeleton/requirements/yaml/.gitkeep +1 -0
- package/templates/android-xml/skeleton/agent-env/skills/harness-android-cli/SKILL.md +88 -0
- package/templates/android-xml/skeleton/agent-env/skills/harness-android-service-patterns/SKILL.md +205 -0
- package/templates/android-xml/skeleton/agent-env/skills/harness-android-xml-architecture/SKILL.md +138 -0
- package/templates/android-xml/skeleton/agent-env/skills/harness-lifecycle-management/SKILL.md +158 -0
- package/templates/android-xml/skeleton/agent-env/skills/harness-xml-ui/SKILL.md +112 -0
- package/templates/cpp/skeleton/agent-env/skills/harness-cmake-build/SKILL.md +163 -0
- package/templates/cpp/skeleton/agent-env/skills/harness-cpp-architecture/SKILL.md +157 -0
- package/templates/cpp/skeleton/agent-env/skills/harness-cpp-concurrency/SKILL.md +180 -0
- package/templates/cpp/skeleton/agent-env/skills/harness-memory-safety/SKILL.md +163 -0
- package/templates/cpp/skeleton/agent-env/skills/harness-modern-cpp/SKILL.md +149 -0
- package/templates/python/skeleton/agent-env/skills/harness-async-patterns/SKILL.md +162 -0
- package/templates/python/skeleton/agent-env/skills/harness-python-architecture/SKILL.md +160 -0
- package/templates/python/skeleton/agent-env/skills/harness-python-package-structure/SKILL.md +210 -0
- package/templates/python/skeleton/agent-env/skills/harness-python-performance/SKILL.md +207 -0
- package/templates/python/skeleton/agent-env/skills/harness-python-testing/SKILL.md +198 -0
- package/templates/svharness.config.example.yaml +39 -0
- package/templates/web-react/skeleton/agent-env/skills/harness-react-architecture/SKILL.md +177 -0
- package/templates/web-react/skeleton/agent-env/skills/harness-react-performance/SKILL.md +177 -0
- package/templates/web-react/skeleton/agent-env/skills/harness-react-testing/SKILL.md +193 -0
- package/templates/web-react/skeleton/agent-env/skills/harness-react-ui-patterns/SKILL.md +257 -0
- package/templates/web-react/skeleton/agent-env/skills/harness-state-management/SKILL.md +189 -0
- package/templates/_shared/skeleton/assets/baseline/code/.gitkeep +0 -1
- package/templates/_shared/skeleton/assets/baseline/wiki/.gitkeep +0 -1
- package/templates/_shared/skeleton/assets/raw/.gitkeep +0 -1
- package/templates/_shared/skeleton/assets/requirements/.gitkeep +0 -1
- /package/templates/_shared/skeleton/{assets/baseline/repomix → agent-env/_incoming/skills}/.gitkeep +0 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runConvert = runConvert;
|
|
7
|
+
/**
|
|
8
|
+
* `svharness convert`
|
|
9
|
+
*
|
|
10
|
+
* Upload local source documents (pdf / docx / pptx / xlsx / html / txt / ...)
|
|
11
|
+
* to a remotely-deployed `markitdown_serve` instance and persist the returned
|
|
12
|
+
* Markdown under `<harness>/requirements/md/`.
|
|
13
|
+
*
|
|
14
|
+
* Design rules (aligned with the approved plan):
|
|
15
|
+
* - The CLI is a pure HTTP client. It never spawns / installs / manages the
|
|
16
|
+
* Python-side service.
|
|
17
|
+
* - Independent subcommand — does NOT run as part of `build`.
|
|
18
|
+
* - Single-file failures do not abort the whole batch; the summary prints
|
|
19
|
+
* success / skipped / failed counts at the end.
|
|
20
|
+
* - Existing output files are kept by default and receive a `-N` suffix on
|
|
21
|
+
* name collision; pass `--force` to overwrite instead.
|
|
22
|
+
*/
|
|
23
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
24
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
25
|
+
const prompts_1 = __importDefault(require("prompts"));
|
|
26
|
+
const logger_1 = require("../utils/logger");
|
|
27
|
+
const validate_args_1 = require("../utils/validate-args");
|
|
28
|
+
const markitdown_client_1 = require("../core/markitdown-client");
|
|
29
|
+
const doc_intake_paths_1 = require("../core/doc-intake-paths");
|
|
30
|
+
/**
|
|
31
|
+
* Extensions recognised by Microsoft MarkItDown (v0.x). Anything outside this
|
|
32
|
+
* whitelist is skipped client-side to avoid a round-trip that is guaranteed to
|
|
33
|
+
* return 415. Keep in sync with the server-side whitelist in
|
|
34
|
+
* `markitdown_serve/app/main.py`.
|
|
35
|
+
*/
|
|
36
|
+
const SUPPORTED_EXTS = new Set([
|
|
37
|
+
'.pdf',
|
|
38
|
+
'.docx',
|
|
39
|
+
'.doc',
|
|
40
|
+
'.pptx',
|
|
41
|
+
'.ppt',
|
|
42
|
+
'.xlsx',
|
|
43
|
+
'.xls',
|
|
44
|
+
'.html',
|
|
45
|
+
'.htm',
|
|
46
|
+
'.epub',
|
|
47
|
+
'.csv',
|
|
48
|
+
'.json',
|
|
49
|
+
'.xml',
|
|
50
|
+
'.txt',
|
|
51
|
+
'.md',
|
|
52
|
+
'.rtf',
|
|
53
|
+
'.msg',
|
|
54
|
+
'.eml',
|
|
55
|
+
'.zip',
|
|
56
|
+
'.jpg',
|
|
57
|
+
'.jpeg',
|
|
58
|
+
'.png',
|
|
59
|
+
'.bmp',
|
|
60
|
+
'.tiff',
|
|
61
|
+
'.mp3',
|
|
62
|
+
'.wav',
|
|
63
|
+
'.m4a',
|
|
64
|
+
]);
|
|
65
|
+
async function runConvert(opts) {
|
|
66
|
+
(0, logger_1.setVerbose)(!!opts.verbose);
|
|
67
|
+
const v = (0, validate_args_1.validateConvertArgs)({
|
|
68
|
+
input: opts.input,
|
|
69
|
+
harness: opts.harness,
|
|
70
|
+
output: opts.output,
|
|
71
|
+
endpoint: opts.endpoint,
|
|
72
|
+
concurrency: opts.concurrency,
|
|
73
|
+
maxFileMB: opts.maxFileMB,
|
|
74
|
+
timeoutSec: opts.timeoutSec,
|
|
75
|
+
type: opts.type,
|
|
76
|
+
});
|
|
77
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
78
|
+
const outDir = node_path_1.default.join(v.harness, v.type, 'md');
|
|
79
|
+
await fs_extra_1.default.ensureDir(outDir);
|
|
80
|
+
logger_1.logger.configBox('svharness convert', [
|
|
81
|
+
{ label: 'harness ', value: v.harness },
|
|
82
|
+
{ label: 'endpoint ', value: v.endpoint },
|
|
83
|
+
{ label: 'type ', value: v.type },
|
|
84
|
+
{ label: 'output dir ', value: outDir },
|
|
85
|
+
{ label: 'concurrency', value: String(v.concurrency) },
|
|
86
|
+
{ label: 'max file ', value: `${v.maxFileMB} MB` },
|
|
87
|
+
{ label: 'timeout ', value: `${v.timeoutSec}s` },
|
|
88
|
+
{ label: 'force ', value: opts.force ? 'yes' : 'no' },
|
|
89
|
+
]);
|
|
90
|
+
// 1. Collect candidate files.
|
|
91
|
+
const candidates = await collectFiles(v.input, cwd);
|
|
92
|
+
if (candidates.length === 0) {
|
|
93
|
+
logger_1.logger.warn('no files matched the given --input patterns');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
logger_1.logger.info(`discovered ${candidates.length} candidate file(s)`);
|
|
97
|
+
// 2. Partition into convertible vs skipped (size / ext).
|
|
98
|
+
const limitBytes = v.maxFileMB * 1024 * 1024;
|
|
99
|
+
const plan = await buildPlan(candidates, limitBytes);
|
|
100
|
+
const skipped = plan.filter((p) => p.status === 'skipped');
|
|
101
|
+
const toUpload = plan.filter((p) => p.status !== 'skipped');
|
|
102
|
+
for (const s of skipped) {
|
|
103
|
+
logger_1.logger.debug(`skip ${s.source}: ${s.reason}`);
|
|
104
|
+
}
|
|
105
|
+
if (skipped.length > 0) {
|
|
106
|
+
logger_1.logger.warn(`skipping ${skipped.length} file(s) locally (unsupported ext or oversized)`);
|
|
107
|
+
}
|
|
108
|
+
if (toUpload.length === 0) {
|
|
109
|
+
logger_1.logger.warn('nothing to upload after local filtering');
|
|
110
|
+
printSummary(skipped.map(planToResult));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// 3. Interactive confirmation (skipped with -y).
|
|
114
|
+
if (!opts.yes) {
|
|
115
|
+
const ans = await (0, prompts_1.default)({
|
|
116
|
+
type: 'confirm',
|
|
117
|
+
name: 'go',
|
|
118
|
+
message: `Upload ${toUpload.length} file(s) to ${v.endpoint}?`,
|
|
119
|
+
initial: true,
|
|
120
|
+
});
|
|
121
|
+
if (!ans.go) {
|
|
122
|
+
logger_1.logger.info('aborted by user');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 4. Probe health endpoint before any upload.
|
|
127
|
+
try {
|
|
128
|
+
const h = await (0, markitdown_client_1.ping)(v.endpoint);
|
|
129
|
+
logger_1.logger.debug(`healthz ok: status=${h.status} markitdown=${h.markitdown_version ?? 'n/a'}`);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const e = err;
|
|
133
|
+
logger_1.logger.error(`markitdown_serve unreachable at ${v.endpoint}: ${e.message}`);
|
|
134
|
+
logger_1.logger.plain('');
|
|
135
|
+
logger_1.logger.plain(' - confirm the service is deployed and reachable from this machine');
|
|
136
|
+
logger_1.logger.plain(' - pass --endpoint <url> or set env SVHARNESS_MARKITDOWN_ENDPOINT');
|
|
137
|
+
logger_1.logger.plain(' - check the server code under svharnessbuild/markitdown_serve/');
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// 5. Bounded-concurrency upload.
|
|
142
|
+
const results = skipped.map(planToResult);
|
|
143
|
+
const used = new Set();
|
|
144
|
+
const queue = toUpload.slice();
|
|
145
|
+
const workers = [];
|
|
146
|
+
const runOne = async () => {
|
|
147
|
+
while (queue.length > 0) {
|
|
148
|
+
const item = queue.shift();
|
|
149
|
+
if (!item)
|
|
150
|
+
break;
|
|
151
|
+
results.push(await uploadOne(item.source, outDir, used, v, opts));
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
for (let i = 0; i < Math.min(v.concurrency, toUpload.length); i++) {
|
|
155
|
+
workers.push(runOne());
|
|
156
|
+
}
|
|
157
|
+
await Promise.all(workers);
|
|
158
|
+
// 6. Summary.
|
|
159
|
+
printSummary(results);
|
|
160
|
+
const relocated = await (0, doc_intake_paths_1.relocateMisplacedConvertedMd)(v.harness, v.type);
|
|
161
|
+
if (relocated > 0) {
|
|
162
|
+
logger_1.logger.warn(`已将 ${relocated} 个误落在 raw/**/converted_md/ 下的 .md 移至 ${v.type}/md/`);
|
|
163
|
+
}
|
|
164
|
+
const failed = results.filter((r) => r.status === 'failed');
|
|
165
|
+
if (failed.length > 0) {
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Walk glob patterns / explicit paths / directories into an absolute file list. */
|
|
170
|
+
async function collectFiles(patterns, cwd) {
|
|
171
|
+
const out = new Set();
|
|
172
|
+
for (const p of patterns) {
|
|
173
|
+
if (isGlob(p)) {
|
|
174
|
+
const matched = await globExpand(p, cwd);
|
|
175
|
+
for (const m of matched)
|
|
176
|
+
out.add(m);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const abs = node_path_1.default.isAbsolute(p) ? p : node_path_1.default.resolve(cwd, p);
|
|
180
|
+
if (!(await fs_extra_1.default.pathExists(abs))) {
|
|
181
|
+
logger_1.logger.warn(`input not found, skipped: ${p}`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const st = await fs_extra_1.default.stat(abs);
|
|
185
|
+
if (st.isDirectory()) {
|
|
186
|
+
for (const f of await walkDir(abs))
|
|
187
|
+
out.add(f);
|
|
188
|
+
}
|
|
189
|
+
else if (st.isFile()) {
|
|
190
|
+
out.add(abs);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return [...out].sort();
|
|
194
|
+
}
|
|
195
|
+
function isGlob(p) {
|
|
196
|
+
return /[*?[\]{}]/.test(p);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Minimal glob expander to avoid pulling in a dependency. Supports the
|
|
200
|
+
* common forms users actually type: `./docs/*.pdf`, `docs/**\/*.docx`,
|
|
201
|
+
* `a/{x,y}.md`. Falls back to treating the pattern as a literal path if no
|
|
202
|
+
* metachars remain after expansion.
|
|
203
|
+
*
|
|
204
|
+
* Implementation: split pattern into base directory (up to first metachar)
|
|
205
|
+
* and the glob tail, then walk the base directory and regex-match each file.
|
|
206
|
+
*/
|
|
207
|
+
async function globExpand(pattern, cwd) {
|
|
208
|
+
// Expand simple brace groups `{a,b,c}` into multiple patterns first.
|
|
209
|
+
const expanded = expandBraces(pattern);
|
|
210
|
+
const results = new Set();
|
|
211
|
+
for (const pat of expanded) {
|
|
212
|
+
for (const f of await globOne(pat, cwd))
|
|
213
|
+
results.add(f);
|
|
214
|
+
}
|
|
215
|
+
return [...results];
|
|
216
|
+
}
|
|
217
|
+
function expandBraces(pattern) {
|
|
218
|
+
const m = /\{([^{}]+)\}/.exec(pattern);
|
|
219
|
+
if (!m)
|
|
220
|
+
return [pattern];
|
|
221
|
+
const [whole, inner] = m;
|
|
222
|
+
const options = inner.split(',');
|
|
223
|
+
const head = pattern.slice(0, m.index);
|
|
224
|
+
const tail = pattern.slice(m.index + whole.length);
|
|
225
|
+
return options.flatMap((opt) => expandBraces(head + opt + tail));
|
|
226
|
+
}
|
|
227
|
+
async function globOne(pattern, cwd) {
|
|
228
|
+
const norm = pattern.replace(/\\/g, '/');
|
|
229
|
+
const firstMeta = norm.search(/[*?[]/);
|
|
230
|
+
if (firstMeta === -1) {
|
|
231
|
+
const abs = node_path_1.default.isAbsolute(norm) ? norm : node_path_1.default.resolve(cwd, norm);
|
|
232
|
+
return (await fs_extra_1.default.pathExists(abs)) ? [abs] : [];
|
|
233
|
+
}
|
|
234
|
+
const lastSlashBeforeMeta = norm.lastIndexOf('/', firstMeta);
|
|
235
|
+
const basePart = lastSlashBeforeMeta === -1 ? '.' : norm.slice(0, lastSlashBeforeMeta) || '/';
|
|
236
|
+
const tail = lastSlashBeforeMeta === -1 ? norm : norm.slice(lastSlashBeforeMeta + 1);
|
|
237
|
+
const baseAbs = node_path_1.default.isAbsolute(basePart)
|
|
238
|
+
? basePart
|
|
239
|
+
: node_path_1.default.resolve(cwd, basePart);
|
|
240
|
+
if (!(await fs_extra_1.default.pathExists(baseAbs)))
|
|
241
|
+
return [];
|
|
242
|
+
const re = globToRegex(tail);
|
|
243
|
+
const all = await walkDir(baseAbs);
|
|
244
|
+
return all.filter((f) => re.test(node_path_1.default.relative(baseAbs, f).replace(/\\/g, '/')));
|
|
245
|
+
}
|
|
246
|
+
function globToRegex(glob) {
|
|
247
|
+
let re = '^';
|
|
248
|
+
for (let i = 0; i < glob.length; i++) {
|
|
249
|
+
const c = glob[i];
|
|
250
|
+
if (c === '*') {
|
|
251
|
+
if (glob[i + 1] === '*') {
|
|
252
|
+
re += '.*';
|
|
253
|
+
i++;
|
|
254
|
+
if (glob[i + 1] === '/')
|
|
255
|
+
i++;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
re += '[^/]*';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (c === '?') {
|
|
262
|
+
re += '[^/]';
|
|
263
|
+
}
|
|
264
|
+
else if ('.+^$()|\\'.includes(c)) {
|
|
265
|
+
re += '\\' + c;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
re += c;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
re += '$';
|
|
272
|
+
return new RegExp(re);
|
|
273
|
+
}
|
|
274
|
+
async function walkDir(dir) {
|
|
275
|
+
const out = [];
|
|
276
|
+
const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
|
|
277
|
+
for (const e of entries) {
|
|
278
|
+
const abs = node_path_1.default.join(dir, e.name);
|
|
279
|
+
if (e.isDirectory()) {
|
|
280
|
+
// Skip harness artifact dirs and noisy dot-dirs.
|
|
281
|
+
if ((0, doc_intake_paths_1.shouldSkipRawSubdir)(e.name)) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
out.push(...(await walkDir(abs)));
|
|
285
|
+
}
|
|
286
|
+
else if (e.isFile()) {
|
|
287
|
+
out.push(abs);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
function planToResult(p) {
|
|
293
|
+
return p.status === 'skipped'
|
|
294
|
+
? { source: p.source, status: 'skipped', reason: p.reason }
|
|
295
|
+
: { source: p.source, status: 'ok' };
|
|
296
|
+
}
|
|
297
|
+
async function buildPlan(files, limitBytes) {
|
|
298
|
+
const out = [];
|
|
299
|
+
for (const f of files) {
|
|
300
|
+
const ext = node_path_1.default.extname(f).toLowerCase();
|
|
301
|
+
if (!SUPPORTED_EXTS.has(ext)) {
|
|
302
|
+
out.push({ source: f, status: 'skipped', reason: `unsupported ext ${ext || '(none)'}` });
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const st = await fs_extra_1.default.stat(f);
|
|
306
|
+
if (st.size > limitBytes) {
|
|
307
|
+
out.push({
|
|
308
|
+
source: f,
|
|
309
|
+
status: 'skipped',
|
|
310
|
+
reason: `exceeds max-file-mb (${Math.round(st.size / 1024 / 1024)}MB)`,
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
out.push({ source: f, status: 'ready' });
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
async function uploadOne(source, outDir, used, v, opts) {
|
|
319
|
+
const basename = node_path_1.default.basename(source, node_path_1.default.extname(source));
|
|
320
|
+
const outPath = await resolveOutputPath(outDir, basename, used, !!opts.force);
|
|
321
|
+
used.add(outPath);
|
|
322
|
+
logger_1.logger.debug(`upload ${source} -> ${outPath}`);
|
|
323
|
+
try {
|
|
324
|
+
const resp = await (0, markitdown_client_1.postConvertWithRetry)(v.endpoint, source, v.timeoutSec * 1000);
|
|
325
|
+
await fs_extra_1.default.writeFile(outPath, resp.markdown, 'utf8');
|
|
326
|
+
const bytes = Buffer.byteLength(resp.markdown, 'utf8');
|
|
327
|
+
logger_1.logger.success(`${node_path_1.default.basename(source)} -> ${node_path_1.default.basename(outPath)} (${bytes}B)`);
|
|
328
|
+
return { source, output: outPath, status: 'ok', bytes };
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
const e = err;
|
|
332
|
+
const reason = e.message;
|
|
333
|
+
logger_1.logger.error(`failed: ${node_path_1.default.basename(source)} — ${reason}`);
|
|
334
|
+
return { source, status: 'failed', reason };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Decide the final .md path. With `--force` we always write to
|
|
339
|
+
* `<basename>.md`; otherwise we append `-1`, `-2`, ... until a free slot is
|
|
340
|
+
* found (also respecting files reserved by the current run via `used`).
|
|
341
|
+
*/
|
|
342
|
+
async function resolveOutputPath(outDir, basename, used, force) {
|
|
343
|
+
const primary = node_path_1.default.join(outDir, `${basename}.md`);
|
|
344
|
+
if (force)
|
|
345
|
+
return primary;
|
|
346
|
+
if (!used.has(primary) && !(await fs_extra_1.default.pathExists(primary)))
|
|
347
|
+
return primary;
|
|
348
|
+
for (let i = 1; i < 1000; i++) {
|
|
349
|
+
const cand = node_path_1.default.join(outDir, `${basename}-${i}.md`);
|
|
350
|
+
if (!used.has(cand) && !(await fs_extra_1.default.pathExists(cand)))
|
|
351
|
+
return cand;
|
|
352
|
+
}
|
|
353
|
+
throw new Error(`unable to allocate unique output filename for ${basename} under ${outDir}`);
|
|
354
|
+
}
|
|
355
|
+
function printSummary(results) {
|
|
356
|
+
const ok = results.filter((r) => r.status === 'ok').length;
|
|
357
|
+
const failed = results.filter((r) => r.status === 'failed').length;
|
|
358
|
+
const skipped = results.filter((r) => r.status === 'skipped').length;
|
|
359
|
+
logger_1.logger.section('convert summary');
|
|
360
|
+
logger_1.logger.plain(` ok : ${ok}`);
|
|
361
|
+
logger_1.logger.plain(` skipped : ${skipped}`);
|
|
362
|
+
logger_1.logger.plain(` failed : ${failed}`);
|
|
363
|
+
if (failed > 0) {
|
|
364
|
+
logger_1.logger.plain('');
|
|
365
|
+
for (const r of results.filter((x) => x.status === 'failed')) {
|
|
366
|
+
logger_1.logger.plain(` - ${node_path_1.default.basename(r.source)}: ${r.reason}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -16,8 +16,12 @@ const state_1 = require("../core/state");
|
|
|
16
16
|
const agent_injector_1 = require("../core/agent-injector");
|
|
17
17
|
const adapters_1 = require("../adapters");
|
|
18
18
|
const next_steps_1 = require("../core/next-steps");
|
|
19
|
+
const build_project_entry_1 = require("../core/build-project-entry");
|
|
19
20
|
const repomix_pack_1 = require("../core/repomix-pack");
|
|
21
|
+
const extra_assets_intake_1 = require("../core/extra-assets-intake");
|
|
20
22
|
const baseline_copy_1 = require("../utils/baseline-copy");
|
|
23
|
+
const doc_intake_paths_1 = require("../core/doc-intake-paths");
|
|
24
|
+
const convert_1 = require("./convert");
|
|
21
25
|
const wiki_1 = require("../wiki");
|
|
22
26
|
/**
|
|
23
27
|
* Resolve the absolute path to the bundled templates/ directory.
|
|
@@ -26,6 +30,42 @@ const wiki_1 = require("../wiki");
|
|
|
26
30
|
function resolveTemplatesRoot() {
|
|
27
31
|
return node_path_1.default.resolve(__dirname, '..', '..', 'templates');
|
|
28
32
|
}
|
|
33
|
+
async function runBuildAutoConvert(targetRoot, rawDir, type, opts) {
|
|
34
|
+
logger_1.logger.info(`开始自动转换 ${type}:${rawDir} -> ${node_path_1.default.join(targetRoot, type, 'md')}`);
|
|
35
|
+
const prevExitCode = process.exitCode;
|
|
36
|
+
process.exitCode = undefined;
|
|
37
|
+
try {
|
|
38
|
+
await (0, convert_1.runConvert)({
|
|
39
|
+
input: [rawDir],
|
|
40
|
+
output: targetRoot,
|
|
41
|
+
type,
|
|
42
|
+
endpoint: opts.convertEndpoint,
|
|
43
|
+
concurrency: opts.convertConcurrency,
|
|
44
|
+
maxFileMB: opts.convertMaxFileMB,
|
|
45
|
+
timeoutSec: opts.convertTimeoutSec,
|
|
46
|
+
force: opts.convertForce,
|
|
47
|
+
yes: true,
|
|
48
|
+
verbose: opts.verbose,
|
|
49
|
+
cwd: opts.cwd,
|
|
50
|
+
});
|
|
51
|
+
const relocated = await (0, doc_intake_paths_1.relocateMisplacedConvertedMd)(targetRoot, type);
|
|
52
|
+
if (relocated > 0) {
|
|
53
|
+
logger_1.logger.warn(`已将 ${relocated} 个误落在 raw/**/converted_md/ 下的 .md 移至 ${type}/md/`);
|
|
54
|
+
}
|
|
55
|
+
if ((process.exitCode ?? 0) !== 0) {
|
|
56
|
+
logger_1.logger.warn(`自动转换 ${type} 存在失败项,已继续 build 流程`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
logger_1.logger.success(`自动转换 ${type} 完成`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
logger_1.logger.warn(`自动转换 ${type} 失败,已继续 build:${err.message}`);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
process.exitCode = prevExitCode;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
29
69
|
async function runInit(opts) {
|
|
30
70
|
(0, logger_1.setVerbose)(!!opts.verbose);
|
|
31
71
|
// 0. Mutual-exclusion between wiki modes.
|
|
@@ -49,11 +89,11 @@ async function runInit(opts) {
|
|
|
49
89
|
const targetRoot = node_path_1.default.resolve(cwd, `${validated.name}-harness`);
|
|
50
90
|
const cliVersion = (0, version_1.getCliVersion)();
|
|
51
91
|
const createdAt = new Date().toISOString();
|
|
52
|
-
// Baseline code lands at <target>/
|
|
92
|
+
// Baseline code lands at <target>/baseline/code/ for both git & local
|
|
53
93
|
// modes. Wiki generation consumes this directory by default so it always
|
|
54
94
|
// reflects the baseline the user asked for (not the caller's cwd).
|
|
55
95
|
const hasBaseline = !!validated.baseline;
|
|
56
|
-
const baselineCodeDir = node_path_1.default.join(targetRoot, '
|
|
96
|
+
const baselineCodeDir = node_path_1.default.join(targetRoot, 'baseline', 'code');
|
|
57
97
|
const wikiSourceRoot = node_path_1.default.resolve(opts.wikiSource ?? (hasBaseline ? baselineCodeDir : cwd));
|
|
58
98
|
// Derive the final wiki mode:
|
|
59
99
|
// - no baseline -> 'off' (wiki flags are warned and ignored)
|
|
@@ -79,6 +119,24 @@ async function runInit(opts) {
|
|
|
79
119
|
{ label: 'Agent', value: validated.agent },
|
|
80
120
|
{ label: '目标路径', value: targetRoot },
|
|
81
121
|
];
|
|
122
|
+
if (opts.requirements) {
|
|
123
|
+
configItems.push({
|
|
124
|
+
label: 'Requirements 路径',
|
|
125
|
+
value: node_path_1.default.resolve(cwd, opts.requirements),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (opts.references) {
|
|
129
|
+
configItems.push({
|
|
130
|
+
label: 'References 路径',
|
|
131
|
+
value: node_path_1.default.resolve(cwd, opts.references),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if ((opts.extraSkills?.length ?? 0) > 0) {
|
|
135
|
+
configItems.push({
|
|
136
|
+
label: 'Extra 资源',
|
|
137
|
+
value: opts.extraSkills.map((item) => node_path_1.default.resolve(cwd, item)).join(', '),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
82
140
|
if (validated.baseline) {
|
|
83
141
|
const modeTag = validated.baselineMode === 'git' ? 'git' : 'local';
|
|
84
142
|
configItems.push({ label: `基线 (${modeTag})`, value: validated.baseline });
|
|
@@ -166,6 +224,8 @@ async function runInit(opts) {
|
|
|
166
224
|
arch: validated.arch,
|
|
167
225
|
agent: validated.agent,
|
|
168
226
|
sourcePath: validated.baseline ?? '',
|
|
227
|
+
requirementsPath: opts.requirements ? node_path_1.default.resolve(cwd, opts.requirements) : '',
|
|
228
|
+
referencesPath: opts.references ? node_path_1.default.resolve(cwd, opts.references) : '',
|
|
169
229
|
createdAt,
|
|
170
230
|
version: cliVersion,
|
|
171
231
|
};
|
|
@@ -175,16 +235,35 @@ async function runInit(opts) {
|
|
|
175
235
|
// optional: baseline copy, repomix pack, wiki generation
|
|
176
236
|
// last: state file
|
|
177
237
|
let stepCursor = 4;
|
|
238
|
+
const hasExtraAssets = (opts.extraSkills?.length ?? 0) > 0;
|
|
239
|
+
const stepExtraAssets = hasExtraAssets ? ++stepCursor : 0;
|
|
178
240
|
const stepBaseline = hasBaseline ? ++stepCursor : 0;
|
|
179
241
|
const stepRepomix = hasBaseline ? ++stepCursor : 0;
|
|
180
242
|
const stepWiki = wikiEnabled ? ++stepCursor : 0;
|
|
181
243
|
const stepState = ++stepCursor;
|
|
182
244
|
const totalSteps = stepCursor;
|
|
183
245
|
let baselineMeta;
|
|
246
|
+
let extraAssetsResult;
|
|
184
247
|
try {
|
|
185
248
|
logger_1.logger.section(`步骤 1/${totalSteps} - 生成目录骨架`);
|
|
186
249
|
const copied = await (0, scaffold_1.scaffoldLayered)(skeletonSrcs, targetRoot);
|
|
187
250
|
logger_1.logger.success(`已拷贝骨架文件 ${copied.length} 个`);
|
|
251
|
+
if (opts.requirements) {
|
|
252
|
+
const requirementRawDir = node_path_1.default.join(targetRoot, 'requirements', 'raw');
|
|
253
|
+
const copiedReq = await (0, doc_intake_paths_1.copySeedInputToRaw)(node_path_1.default.resolve(cwd, opts.requirements), requirementRawDir, 'requirements');
|
|
254
|
+
logger_1.logger.success(`已注入 requirements 输入到 raw:${copiedReq} 项`);
|
|
255
|
+
if (copiedReq > 0) {
|
|
256
|
+
await runBuildAutoConvert(targetRoot, requirementRawDir, 'requirements', opts);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (opts.references) {
|
|
260
|
+
const referenceRawDir = node_path_1.default.join(targetRoot, 'references', 'raw');
|
|
261
|
+
const copiedRef = await (0, doc_intake_paths_1.copySeedInputToRaw)(node_path_1.default.resolve(cwd, opts.references), referenceRawDir, 'references');
|
|
262
|
+
logger_1.logger.success(`已注入 references 输入到 raw:${copiedRef} 项`);
|
|
263
|
+
if (copiedRef > 0) {
|
|
264
|
+
await runBuildAutoConvert(targetRoot, referenceRawDir, 'references', opts);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
188
267
|
logger_1.logger.section(`步骤 2/${totalSteps} - 渲染元文件`);
|
|
189
268
|
const meta = await (0, render_meta_1.renderMetaLayered)(metaSrcs, targetRoot, ctx);
|
|
190
269
|
logger_1.logger.success(`已渲染元文件 ${meta.length} 个: ${meta.join(', ')}`);
|
|
@@ -193,16 +272,34 @@ async function runInit(opts) {
|
|
|
193
272
|
// Skills are injected beside the harness folder (cwd), not inside it,
|
|
194
273
|
// so the Agent picks them up from the project root.
|
|
195
274
|
const harnessDirName = node_path_1.default.basename(targetRoot);
|
|
196
|
-
const
|
|
275
|
+
const buildSkillsBinding = {
|
|
276
|
+
harnessDirName,
|
|
277
|
+
harnessName: validated.name,
|
|
278
|
+
arch: validated.arch,
|
|
279
|
+
agent: validated.agent,
|
|
280
|
+
cliVersion,
|
|
281
|
+
generatedAt: createdAt,
|
|
282
|
+
};
|
|
283
|
+
const injected = await (0, agent_injector_1.injectSkillsLayered)(buildSkillsSrcs, targetRoot, adapter, cwd, buildSkillsBinding);
|
|
197
284
|
logger_1.logger.success(`已向 ${node_path_1.default.join(cwd, adapter.skillsDir)}/ 注入 skill ${injected.length} 个`);
|
|
198
285
|
logger_1.logger.section(`步骤 4/${totalSteps} - 注入 harness 构建规则`);
|
|
199
286
|
const rules = await (0, agent_injector_1.injectRulesLayered)(buildRulesSrcs, adapter, cwd);
|
|
200
287
|
if (adapter.rulesDir) {
|
|
201
288
|
logger_1.logger.success(`已向 ${node_path_1.default.join(cwd, adapter.rulesDir)}/ 注入 rule ${rules.length} 个`);
|
|
202
289
|
}
|
|
290
|
+
if (hasExtraAssets && opts.extraSkills) {
|
|
291
|
+
logger_1.logger.section(`步骤 ${stepExtraAssets}/${totalSteps} - 导入额外运行期资源到 _incoming`);
|
|
292
|
+
extraAssetsResult = await (0, extra_assets_intake_1.intakeExtraAssets)({
|
|
293
|
+
inputs: opts.extraSkills,
|
|
294
|
+
cwd,
|
|
295
|
+
harnessRoot: targetRoot,
|
|
296
|
+
});
|
|
297
|
+
logger_1.logger.success(`incoming 清单: ${extraAssetsResult.manifestRelPath}(总计 ${extraAssetsResult.incomingTotal},` +
|
|
298
|
+
`skills ${extraAssetsResult.skillsCount},rules ${extraAssetsResult.rulesCount},unknown ${extraAssetsResult.unknownCount})`);
|
|
299
|
+
}
|
|
203
300
|
if (hasBaseline && validated.baseline && validated.baselineMode) {
|
|
204
301
|
logger_1.logger.section(`步骤 ${stepBaseline}/${totalSteps} - 拷贝基线源码`);
|
|
205
|
-
const destDir = node_path_1.default.join(targetRoot, '
|
|
302
|
+
const destDir = node_path_1.default.join(targetRoot, 'baseline', 'code');
|
|
206
303
|
baselineMeta = await (0, baseline_copy_1.copyBaseline)({
|
|
207
304
|
mode: validated.baselineMode,
|
|
208
305
|
source: validated.baseline,
|
|
@@ -222,6 +319,18 @@ async function runInit(opts) {
|
|
|
222
319
|
await (0, scaffold_1.rollbackTarget)(targetRoot);
|
|
223
320
|
throw err;
|
|
224
321
|
}
|
|
322
|
+
{
|
|
323
|
+
const adapterEntry = (0, adapters_1.getAdapter)(validated.agent);
|
|
324
|
+
await (0, build_project_entry_1.writeBuildProjectEntry)({
|
|
325
|
+
projectRoot: cwd,
|
|
326
|
+
adapter: adapterEntry,
|
|
327
|
+
harnessDirName: node_path_1.default.basename(targetRoot),
|
|
328
|
+
harnessName: validated.name,
|
|
329
|
+
arch: validated.arch,
|
|
330
|
+
agent: validated.agent,
|
|
331
|
+
force: !!opts.force,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
225
334
|
// 6a. Repomix XML pack of baseline code (default when --baseline; non-fatal on failure).
|
|
226
335
|
if (hasBaseline) {
|
|
227
336
|
logger_1.logger.section(`步骤 ${stepRepomix}/${totalSteps} - 生成 baseline Repomix 包`);
|
|
@@ -258,7 +367,7 @@ async function runInit(opts) {
|
|
|
258
367
|
try {
|
|
259
368
|
const repoRootFs = wikiSourceRoot;
|
|
260
369
|
const wikiOutputRootFs = targetRoot;
|
|
261
|
-
const wikiRelPath = node_path_1.default.join('
|
|
370
|
+
const wikiRelPath = node_path_1.default.join('baseline', 'wiki');
|
|
262
371
|
// Guard: baseline wiki must be based on real code. If the user provided
|
|
263
372
|
// --baseline but the copy step produced an empty directory (e.g. every
|
|
264
373
|
// file was filtered out), refuse to silently index cwd or an empty dir.
|
|
@@ -311,12 +420,12 @@ async function runInit(opts) {
|
|
|
311
420
|
logger_1.logger.warn(`wiki 生成失败(不回滚 harness 骨架):${err.message}`);
|
|
312
421
|
}
|
|
313
422
|
}
|
|
314
|
-
// If the full run failed, downgrade
|
|
423
|
+
// If the full run failed, downgrade S10_wiki from DONE to PENDING
|
|
315
424
|
// so the agent can pick it up via harness-build-skill-wiki-writer.
|
|
316
425
|
if (!wikiFullOk) {
|
|
317
426
|
resolvedWikiPhase = 'pending';
|
|
318
427
|
resolvedWikiSource = 'cli-full-degraded';
|
|
319
|
-
logger_1.logger.warn('
|
|
428
|
+
logger_1.logger.warn('S10_wiki 将标记为 PENDING,等待 agent 接力补齐');
|
|
320
429
|
}
|
|
321
430
|
}
|
|
322
431
|
else if (wikiMode === 'tasks') {
|
|
@@ -324,7 +433,7 @@ async function runInit(opts) {
|
|
|
324
433
|
try {
|
|
325
434
|
const repoRootFs = wikiSourceRoot;
|
|
326
435
|
const wikiOutputRootFs = targetRoot;
|
|
327
|
-
const wikiRelPath = node_path_1.default.join('
|
|
436
|
+
const wikiRelPath = node_path_1.default.join('baseline', 'wiki');
|
|
328
437
|
if (hasBaseline) {
|
|
329
438
|
const exists = await fs_extra_1.default.pathExists(repoRootFs);
|
|
330
439
|
const entries = exists ? await fs_extra_1.default.readdir(repoRootFs) : [];
|
|
@@ -400,6 +509,14 @@ async function runInit(opts) {
|
|
|
400
509
|
wikiPhase: resolvedWikiPhase,
|
|
401
510
|
wikiSource: resolvedWikiSource,
|
|
402
511
|
});
|
|
512
|
+
if (extraAssetsResult) {
|
|
513
|
+
const s65 = state.phases.S65_customize_agent_env;
|
|
514
|
+
s65.incoming_manifest = extraAssetsResult.manifestRelPath;
|
|
515
|
+
s65.incoming_total = extraAssetsResult.incomingTotal;
|
|
516
|
+
s65.skills_count = extraAssetsResult.skillsCount;
|
|
517
|
+
s65.rules_count = extraAssetsResult.rulesCount;
|
|
518
|
+
s65.unknown_count = extraAssetsResult.unknownCount;
|
|
519
|
+
}
|
|
403
520
|
const statePath = await (0, state_1.writeStateFile)(targetRoot, state);
|
|
404
521
|
logger_1.logger.success(`状态文件: ${node_path_1.default.relative(targetRoot, statePath)}`);
|
|
405
522
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runReferencesApplySkills = runReferencesApplySkills;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const prompts_1 = __importDefault(require("prompts"));
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
const reference_apply_skills_1 = require("../core/reference-apply-skills");
|
|
11
|
+
async function runReferencesApplySkills(opts) {
|
|
12
|
+
(0, logger_1.setVerbose)(!!opts.verbose);
|
|
13
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
14
|
+
const harnessRoot = node_path_1.default.resolve(cwd, opts.harness);
|
|
15
|
+
logger_1.logger.configBox('references → apply skills', [
|
|
16
|
+
{ label: 'harness', value: harnessRoot },
|
|
17
|
+
{ label: 'force', value: opts.force ? 'yes' : 'no' },
|
|
18
|
+
]);
|
|
19
|
+
if (!opts.yes) {
|
|
20
|
+
const { ok } = await (0, prompts_1.default)({
|
|
21
|
+
type: 'confirm',
|
|
22
|
+
name: 'ok',
|
|
23
|
+
message: '根据 references/yaml/index.yaml 生成 harness-apply-skills-* 并写入 registry?',
|
|
24
|
+
initial: true,
|
|
25
|
+
});
|
|
26
|
+
if (!ok) {
|
|
27
|
+
logger_1.logger.warn('用户取消');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const result = await (0, reference_apply_skills_1.materializeReferenceApplySkills)(harnessRoot, {
|
|
32
|
+
force: opts.force,
|
|
33
|
+
});
|
|
34
|
+
logger_1.logger.success(`registry: ${node_path_1.default.relative(harnessRoot, result.registryPath)}`);
|
|
35
|
+
if (result.created.length > 0) {
|
|
36
|
+
logger_1.logger.success(`新建 skill: ${result.created.join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
if (result.updated.length > 0) {
|
|
39
|
+
logger_1.logger.success(`更新 skill: ${result.updated.join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
if (result.skipped.length > 0) {
|
|
42
|
+
logger_1.logger.warn(`跳过(已存在,未使用 --force): ${result.skipped.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
logger_1.logger.plain('');
|
|
45
|
+
logger_1.logger.plain('下一步:svharness apply --harness <path> --target <project> --yes');
|
|
46
|
+
logger_1.logger.plain(' apply 会把 registry 中的 skill 注入目标 Agent,并注册到 harness-apply-skills-main');
|
|
47
|
+
}
|