mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-17 13:45:34 +02:00
feat: add DeepSeek Harness guide, hooks generator, and setup skill
- docs/DEEPSEEK-HARNESS-GUIDE.md: skills roots, hooks bridge mounting, loader rules learned from practice, scope table | - scripts/dsh/generate-hooks-config.js: convert hooks/hooks.json to the dsh-hooks-claude-code bridge format (lower-case tool matchers, inline plugin-root env prefix, timeout preserved, unsupported events skipped) | - skills/deepseek-harness-setup: skills-first entry point per repo policy | - tests/scripts/dsh-generate-hooks-config.test.js: 8 unit + CLI tests
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
# DeepSeek Harness Guide
|
||||
|
||||
ECC runs on the [DeepSeek Harness](https://github.com/deepseek-ai/dsh) (DSH) via its
|
||||
native skill roots and the first-party Claude Code hooks bridge. This guide covers
|
||||
what works out of the box, how to mount the ECC hooks, and the current limits.
|
||||
|
||||
Unlike the Qwen or Codex adapters, there is no dedicated installer target yet; the
|
||||
setup is a small amount of manual configuration described below.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- DeepSeek Harness 0.2.0-rc.1 or newer (`dsh --version`).
|
||||
- ECC repository cloned locally, for example `~/ECC` (the hooks bridge commands
|
||||
reference this path, so keep the checkout in place).
|
||||
- Skills need no extra tooling; hooks use the `@deepseek-ai/dsh-hooks-claude-code`
|
||||
plugin that ships inside the DSH runtime itself.
|
||||
|
||||
## Skills (automatic)
|
||||
|
||||
DSH loads skills from these roots (earlier roots win on name conflicts):
|
||||
|
||||
```text
|
||||
<project>/.dsh/skills
|
||||
<project>/.agents/skills
|
||||
custom dirs (~/.dsh/profiles/<profile>/cordis.patch.yml -> skill-filesystem)
|
||||
~/.dsh/skills
|
||||
~/.agents/skills
|
||||
```
|
||||
|
||||
Because ECC already ships `.agents/skills/` for Codex, opening a session with the
|
||||
ECC checkout as the working directory exposes a subset of ECC skills immediately.
|
||||
To expose the full curated set in every project, link the catalog once:
|
||||
|
||||
```bash
|
||||
ln -s /path/to/ECC/skills/<skill-name> ~/.dsh/skills/<skill-name>
|
||||
```
|
||||
|
||||
DSH reads `SKILL.md` frontmatter (name/description), which matches the ECC skill
|
||||
format, so no conversion is needed. Name conflicts with pre-existing user skills
|
||||
are resolved by root priority; skip those skills when linking.
|
||||
|
||||
## Hooks
|
||||
|
||||
### How the bridge works
|
||||
|
||||
DSH bridges ECC command hooks through `@deepseek-ai/dsh-hooks-claude-code`
|
||||
(first-party, bundled with the runtime). Constraints that shape the conversion:
|
||||
|
||||
- Supported events: `SessionStart`, `UserPromptSubmit`, `PreToolUse`,
|
||||
`PostToolUse`, `Stop`, `SubagentStart`, `SubagentStop`. ECC events without a
|
||||
bridge counterpart (`PreCompact`, `PostToolUseFailure`, `SessionEnd`) are
|
||||
skipped by the generator.
|
||||
- DSH tool names are lower-case (`bash`, `edit`, `write`, `skill`, `task`),
|
||||
while ECC matchers use Claude Code names (`Bash`, `Edit|MultiEdit`, ...). The
|
||||
generator maps known tool names and keeps regex passthrough tokens.
|
||||
- The bridge runs commands with `bash -c` and does not export the plugin root as
|
||||
an environment variable for hook processes. The generator prefixes each command
|
||||
with `CLAUDE_PLUGIN_ROOT=<ecc-root> ECC_PLUGIN_ROOT=<ecc-root>` (env-prefix
|
||||
form), which the ECC hook bootstrap reads at startup.
|
||||
- `timeout` values are seconds in both formats and are preserved.
|
||||
|
||||
### Generate the config
|
||||
|
||||
```bash
|
||||
node scripts/dsh/generate-hooks-config.js --ecc-root /path/to/ECC --out ~/.dsh/ecc/hooks-dsh.json
|
||||
```
|
||||
|
||||
The script validates nothing about your DSH install; it only converts
|
||||
`hooks/hooks.json` into the bridge format and prints what it skipped.
|
||||
|
||||
### Mount into a profile
|
||||
|
||||
Add a bare insert entry to the profile patch layer
|
||||
(`~/.dsh/profiles/<profile>/cordis.patch.yml`). This mirrors how the bundled web
|
||||
app preset mounts its plugins:
|
||||
|
||||
```yaml
|
||||
- insert:
|
||||
- id: ecc-hooks-claude-code
|
||||
name: "@deepseek-ai/dsh-hooks-claude-code"
|
||||
config:
|
||||
configPath: /absolute/path/to/hooks-dsh.json
|
||||
```
|
||||
|
||||
Configuration notes from practice:
|
||||
|
||||
- A plain top-level entry (`- id: ...`) is silently dropped; the loader only
|
||||
treats `insert` entries as additions.
|
||||
- `insert` under an existing non-group id is rejected ("not a group"); use a bare
|
||||
insert without a target id.
|
||||
- The plugin package does not need to be added to the profile `package.json`;
|
||||
first-party plugins resolve from the DSH runtime install.
|
||||
- Validate without booting: `cd ~/.dsh && dsh --profile <profile> --dump-config`
|
||||
(compose errors and dropped entries are reported on stderr).
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
# Compose check (no session boot)
|
||||
cd ~/.dsh && dsh --profile web --dump-config > /dev/null && echo OK
|
||||
|
||||
# End-to-end: fire a SessionStart hook in a one-shot headless session
|
||||
# (uses a test config with a single marker-writing hook)
|
||||
dsh --profile headless --patch /tmp/ecc-patch-test.yml 'Reply with exactly: BOOT-OK'
|
||||
```
|
||||
|
||||
In a live web session, the GateGuard fact-forcing hook denies the first
|
||||
edit/write/bash attempt until facts are presented — a visible signal that the
|
||||
bridge is active.
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
cd /path/to/ECC && git pull
|
||||
node scripts/dsh/generate-hooks-config.js --ecc-root /path/to/ECC --out ~/.dsh/ecc/hooks-dsh.json
|
||||
```
|
||||
|
||||
Linked skills reflect the pull immediately (symlinks), only the hooks config
|
||||
needs regeneration.
|
||||
|
||||
## Scope
|
||||
|
||||
| Surface | Status on DSH |
|
||||
|---------|---------------|
|
||||
| Skills (293) | Works; link or rely on `.agents/skills` |
|
||||
| Hooks (20 command hooks, 4 of 7 events) | Works via the bridge; 3 events unsupported |
|
||||
| Commands (94 slash commands) | Not available (Claude Code mechanism); workflows are covered by skills |
|
||||
| Agents (68) | Not available as subagents; equivalent personas exist as skills |
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate a DeepSeek Harness (DSH) hooks config from hooks/hooks.json.
|
||||
*
|
||||
* DSH bridges ECC command hooks through the first-party plugin
|
||||
* @deepseek-ai/dsh-hooks-claude-code. The bridge supports a subset of
|
||||
* Claude Code hook events and lower-cases tool names in matchers, so this
|
||||
* script converts the canonical ECC hooks.json into a DSH-compatible file.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/dsh/generate-hooks-config.js [--ecc-root <dir>] [--out <file>]
|
||||
*
|
||||
* Defaults:
|
||||
* --ecc-root directory of this checkout (repo root)
|
||||
* --out <ecc-root>/hooks/hooks.dsh.json
|
||||
*
|
||||
* The output is a Claude Code hooks-format file consumed by the bridge:
|
||||
* { "hooks": { "PreToolUse": [ { "matcher": "...", "hooks": [...] } ] } }
|
||||
*
|
||||
* Unsupported ECC events (no DSH bridge counterpart) are skipped and
|
||||
* reported on stderr: PreCompact, PostToolUseFailure, SessionEnd.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SUPPORTED_EVENTS = new Set([
|
||||
'SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop',
|
||||
'SubagentStart', 'SubagentStop',
|
||||
]);
|
||||
|
||||
// Claude Code tool names -> DSH lower-case tool names used by the bridge.
|
||||
const TOOL_NAME_MAP = {
|
||||
Bash: 'bash',
|
||||
PowerShell: 'powershell',
|
||||
Write: 'write',
|
||||
Edit: 'edit',
|
||||
MultiEdit: 'edit',
|
||||
Skill: 'skill',
|
||||
Task: 'task',
|
||||
};
|
||||
|
||||
function parseArgs(argv) {
|
||||
const opts = { eccRoot: null, out: null };
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === '--ecc-root') opts.eccRoot = argv[++i];
|
||||
else if (a === '--out') opts.out = argv[++i];
|
||||
else {
|
||||
process.stderr.write('Unknown argument: ' + a + '\n');
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a Claude Code matcher expression to DSH tool-name tokens.
|
||||
* Split on |, map known tool names to lower-case DSH names, keep regex
|
||||
* passthrough tokens (like .* or ^mcp__) untouched, de-duplicate.
|
||||
*/
|
||||
function mapMatcher(matcher) {
|
||||
if (typeof matcher !== 'string' || matcher.length === 0) return matcher;
|
||||
const parts = matcher.split('|').map(function (t) {
|
||||
const v = t.trim();
|
||||
if (v === '' || v === '.*' || v === '^mcp__') return v;
|
||||
const mapped = TOOL_NAME_MAP[v];
|
||||
return mapped !== undefined ? mapped : v.toLowerCase();
|
||||
});
|
||||
return Array.from(new Set(parts)).join('|');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite an ECC command into a DSH bridge command.
|
||||
*
|
||||
* The bridge runs commands with bash -c but does not export the plugin
|
||||
* root as an environment variable for every hook process. Each ECC hook
|
||||
* command embeds a resolver (`node -e "..." <hook-id> <script> [args]`)
|
||||
* that resolves scripts/hooks/plugin-hook-bootstrap.js from the
|
||||
* CLAUDE_PLUGIN_ROOT environment variable, so prefixing the variables
|
||||
* inline (env-prefix form) is the only required change.
|
||||
*/
|
||||
function mapCommand(command, eccRoot) {
|
||||
const prefix = 'CLAUDE_PLUGIN_ROOT=' + JSON.stringify(eccRoot) +
|
||||
' ECC_PLUGIN_ROOT=' + JSON.stringify(eccRoot) + ' ';
|
||||
return prefix + command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the ECC hooks document into the DSH bridge format.
|
||||
* Returns { config, report } where report lists skipped events/entries.
|
||||
*/
|
||||
function generateConfig(eccRoot, sourceDoc) {
|
||||
const out = { hooks: {} };
|
||||
const report = [];
|
||||
for (const entry of Object.entries(sourceDoc.hooks || {})) {
|
||||
const event = entry[0];
|
||||
const groups = entry[1];
|
||||
if (!SUPPORTED_EVENTS.has(event)) {
|
||||
report.push('skip event ' + event + ' (not bridged by DSH)');
|
||||
continue;
|
||||
}
|
||||
const outGroups = [];
|
||||
for (const group of groups) {
|
||||
const hooks = [];
|
||||
for (const h of group.hooks || []) {
|
||||
if (h.type !== 'command' || typeof h.command !== 'string') {
|
||||
report.push('skip non-command hook on ' + event);
|
||||
continue;
|
||||
}
|
||||
const mapped = { type: 'command', command: mapCommand(h.command, eccRoot) };
|
||||
if (typeof h.timeout === 'number') mapped.timeout = h.timeout;
|
||||
hooks.push(mapped);
|
||||
report.push('ok ' + event);
|
||||
}
|
||||
if (hooks.length) {
|
||||
outGroups.push({ matcher: mapMatcher(group.matcher), hooks: hooks });
|
||||
}
|
||||
}
|
||||
if (outGroups.length) out.hooks[event] = outGroups;
|
||||
}
|
||||
return { config: out, report: report };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const opts = parseArgs(process.argv);
|
||||
const eccRoot = path.resolve(opts.eccRoot || path.join(__dirname, '..', '..'));
|
||||
const sourcePath = path.join(eccRoot, 'hooks', 'hooks.json');
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
process.stderr.write('ECC hooks.json not found at ' + sourcePath + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
let doc;
|
||||
try {
|
||||
doc = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
|
||||
} catch (err) {
|
||||
process.stderr.write('Failed to parse ' + sourcePath + ': ' + err.message + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
const result = generateConfig(eccRoot, doc);
|
||||
const outPath = opts.out || path.join(eccRoot, 'hooks', 'hooks.dsh.json');
|
||||
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
||||
fs.writeFileSync(outPath, JSON.stringify(result.config, null, 2) + '\n');
|
||||
const okCount = result.report.filter(function (r) { return r.indexOf('ok ') === 0; }).length;
|
||||
const notes = result.report.filter(function (r) { return r.indexOf('ok ') !== 0; });
|
||||
process.stderr.write('wrote ' + outPath + '\n');
|
||||
process.stderr.write('hook commands: ' + okCount + '\n');
|
||||
if (notes.length) process.stderr.write(notes.join('; ') + '\n');
|
||||
}
|
||||
|
||||
module.exports = { generateConfig, mapMatcher, mapCommand, SUPPORTED_EVENTS, TOOL_NAME_MAP };
|
||||
|
||||
if (require.main === module) main();
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: deepseek-harness-setup
|
||||
description: "Install and operate Everything Claude Code (ECC) on the DeepSeek Harness (DSH): native skill roots (~/.dsh/skills, .agents/skills), the @deepseek-ai/dsh-hooks-claude-code bridge for command hooks, bare-insert patch mounting, generator usage, event-support limits, and update workflow. Use when setting up ECC on DeepSeek Harness, wiring ECC hooks into a DSH profile, or debugging why ECC skills or hooks do not appear in a DSH session."
|
||||
metadata:
|
||||
origin: community
|
||||
---
|
||||
|
||||
# DeepSeek Harness Setup
|
||||
|
||||
Run ECC on the DeepSeek Harness. Full guide: `docs/DEEPSEEK-HARNESS-GUIDE.md`.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Installing ECC skills or hooks on a DeepSeek Harness install.
|
||||
- Deciding which ECC surfaces work on DSH and which do not.
|
||||
- Debugging a DSH session that does not show ECC skills or does not fire ECC hooks.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Skills need no conversion.** DSH reads `SKILL.md` frontmatter
|
||||
(name/description) from native skill roots. Link the ECC catalog once:
|
||||
|
||||
```bash
|
||||
ln -s /path/to/ECC/skills/<skill-name> ~/.dsh/skills/<skill-name>
|
||||
```
|
||||
|
||||
Root priority (project `.dsh` and `.agents` roots beat `~/.dsh`): skip
|
||||
linking skills whose names already exist in a higher-priority root.
|
||||
|
||||
2. **Hooks go through the first-party bridge.** Generate a DSH-format config
|
||||
from the canonical ECC hooks file:
|
||||
|
||||
```bash
|
||||
node scripts/dsh/generate-hooks-config.js --ecc-root /path/to/ECC --out ~/.dsh/ecc/hooks-dsh.json
|
||||
```
|
||||
|
||||
The generator lower-cases tool-name matchers, prefixes
|
||||
`CLAUDE_PLUGIN_ROOT`/`ECC_PLUGIN_ROOT` inline (the bridge does not export
|
||||
the plugin root), preserves timeouts, and skips events DSH cannot bridge:
|
||||
`PreCompact`, `PostToolUseFailure`, `SessionEnd`.
|
||||
|
||||
3. **Mount with a bare insert** in `~/.dsh/profiles/<profile>/cordis.patch.yml`:
|
||||
|
||||
```yaml
|
||||
- insert:
|
||||
- id: ecc-hooks-claude-code
|
||||
name: "@deepseek-ai/dsh-hooks-claude-code"
|
||||
config:
|
||||
configPath: /absolute/path/to/hooks-dsh.json
|
||||
```
|
||||
|
||||
Loader rules that cost debugging time when learned the hard way:
|
||||
a plain `- id:` top-level entry is silently dropped; `insert` under an
|
||||
existing non-group id is rejected as "not a group"; first-party plugin
|
||||
packages need no profile `package.json` entry.
|
||||
|
||||
4. **Validate by composing, then verify live.**
|
||||
|
||||
```bash
|
||||
cd ~/.dsh && dsh --profile web --dump-config > /dev/null && echo OK
|
||||
```
|
||||
|
||||
A live GateGuard denial on the session's first edit/write/bash is the
|
||||
visible signal the bridge is active.
|
||||
|
||||
## Updating
|
||||
|
||||
`git pull` reflects into linked skills immediately; regenerate the hooks
|
||||
config with the same generator command. If the ECC checkout moves, regenerate
|
||||
(the path is embedded in every hook command) and relink skills.
|
||||
|
||||
## Limits
|
||||
|
||||
| Surface | Status on DSH |
|
||||
|---------|---------------|
|
||||
| Skills | Full catalog works |
|
||||
| Hooks | 20 command hooks on 4 of 7 bridge events |
|
||||
| Slash commands | Not available (Claude Code mechanism); workflows covered by skills |
|
||||
| Agents | Not available as subagents; equivalent personas exist as skills |
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Tests for scripts/dsh/generate-hooks-config.js
|
||||
*
|
||||
* Run with: node tests/scripts/dsh-generate-hooks-config.test.js
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const gen = require('../../scripts/dsh/generate-hooks-config');
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(' \u2713 ' + name);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(' \u2717 ' + name);
|
||||
console.log(' Error: ' + err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
if (test('mapMatcher maps Claude tool names to DSH lower-case names', () => {
|
||||
assert.strictEqual(gen.mapMatcher('Bash'), 'bash');
|
||||
assert.strictEqual(gen.mapMatcher('Edit|MultiEdit'), 'edit');
|
||||
assert.strictEqual(gen.mapMatcher('Write|Edit|MultiEdit'), 'write|edit');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('mapMatcher keeps regex passthrough tokens untouched', () => {
|
||||
assert.strictEqual(gen.mapMatcher('.*'), '.*');
|
||||
assert.strictEqual(gen.mapMatcher('^mcp__'), '^mcp__');
|
||||
assert.strictEqual(gen.mapMatcher('.*|Task'), '.*|task');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('mapCommand prefixes plugin-root env vars and keeps the resolver intact', () => {
|
||||
const cmd = 'node -e "resolver" pretooluse:bash:check-console-log scripts/hooks/check-console-log.js standard';
|
||||
const out = gen.mapCommand(cmd, '/ecc');
|
||||
assert.strictEqual(out, 'CLAUDE_PLUGIN_ROOT="/ecc" ECC_PLUGIN_ROOT="/ecc" ' + cmd);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('mapCommand passes through non-resolver commands with prefix only', () => {
|
||||
const out = gen.mapCommand('echo hi', '/ecc');
|
||||
assert.strictEqual(out, 'CLAUDE_PLUGIN_ROOT="/ecc" ECC_PLUGIN_ROOT="/ecc" echo hi');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('generateConfig skips unsupported events and reports them', () => {
|
||||
const doc = { hooks: {
|
||||
PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: 'node -e "x" node scripts/hooks/a.js' }] }],
|
||||
PreCompact: [{ matcher: '.*', hooks: [{ type: 'command', command: 'echo hi' }] }],
|
||||
SessionEnd: [{ matcher: '.*', hooks: [{ type: 'command', command: 'echo hi' }] }],
|
||||
} };
|
||||
const { config, report } = gen.generateConfig('/ecc', doc);
|
||||
assert.deepStrictEqual(Object.keys(config.hooks), ['PreToolUse']);
|
||||
assert.ok(report.some(r => r.includes('PreCompact')));
|
||||
assert.ok(report.some(r => r.includes('SessionEnd')));
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('generateConfig preserves timeout and maps matcher groups', () => {
|
||||
const doc = { hooks: {
|
||||
PostToolUse: [{ matcher: 'Edit|MultiEdit|Write', hooks: [
|
||||
{ type: 'command', command: 'node -e "x" node scripts/hooks/b.js', timeout: 30 },
|
||||
] }],
|
||||
} };
|
||||
const { config } = gen.generateConfig('/ecc', doc);
|
||||
const g = config.hooks.PostToolUse[0];
|
||||
assert.strictEqual(g.matcher, 'edit|write');
|
||||
assert.strictEqual(g.hooks[0].timeout, 30);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('generateConfig supports the bridge-only Subagent events', () => {
|
||||
assert.ok(gen.SUPPORTED_EVENTS.has('SubagentStart'));
|
||||
assert.ok(gen.SUPPORTED_EVENTS.has('SubagentStop'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('CLI end-to-end writes valid JSON to --out', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-dsh-'));
|
||||
const outPath = path.join(tmp, 'hooks.dsh.json');
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
const res = spawnSync('node', [
|
||||
path.join(repoRoot, 'scripts', 'dsh', 'generate-hooks-config.js'),
|
||||
'--ecc-root', repoRoot, '--out', outPath,
|
||||
], { encoding: 'utf8' });
|
||||
assert.strictEqual(res.status, 0, 'CLI exit 0, stderr: ' + res.stderr);
|
||||
const doc = JSON.parse(fs.readFileSync(outPath, 'utf8'));
|
||||
assert.ok(doc.hooks.PreToolUse.length > 0, 'PreToolUse groups present');
|
||||
assert.ok(Object.keys(doc.hooks).includes('SessionStart'));
|
||||
assert.ok(!Object.keys(doc.hooks).includes('PreCompact'), 'unsupported event excluded');
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log('');
|
||||
console.log('Passed: ' + passed + ', Failed: ' + failed);
|
||||
return failed === 0;
|
||||
}
|
||||
|
||||
process.exit(runTests() ? 0 : 1);
|
||||
Reference in New Issue
Block a user