dsh-remote 0.8.50 → 0.8.52
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.en.md +8 -0
- package/README.md +22 -0
- package/lib/client.js +15 -3
- package/lib/index.js +269 -8
- package/lib/instructions.js +288 -0
- package/lib/openssh.js +10 -1
- package/lib/pool.js +29 -1
- package/lib/remote-fs.js +26 -1
- package/lib/sftp-surface.js +46 -3
- package/lib/sshconfig.js +13 -3
- package/node_modules/buildcheck/.eslintrc.js +5 -0
- package/node_modules/buildcheck/.github/workflows/ci.yml +88 -0
- package/node_modules/buildcheck/.github/workflows/lint.yml +27 -0
- package/node_modules/buildcheck/.npmignore +3 -0
- package/node_modules/buildcheck/LICENSE +19 -0
- package/node_modules/buildcheck/README.md +106 -0
- package/node_modules/buildcheck/deps/Find-VisualStudio.cs +250 -0
- package/node_modules/buildcheck/lib/findvs.js +361 -0
- package/node_modules/buildcheck/lib/index.js +738 -0
- package/node_modules/buildcheck/package.json +36 -0
- package/node_modules/buildcheck/test/test.js +8 -0
- package/node_modules/cpu-features/.eslintrc.js +5 -0
- package/node_modules/cpu-features/.github/workflows/ci.yml +74 -0
- package/node_modules/cpu-features/.github/workflows/lint.yml +27 -0
- package/node_modules/cpu-features/LICENSE +19 -0
- package/node_modules/cpu-features/README.md +59 -0
- package/node_modules/cpu-features/binding.gyp +16 -0
- package/node_modules/cpu-features/buildcheck.js +32 -0
- package/node_modules/cpu-features/deps/cpu_features/.clang-format +4 -0
- package/node_modules/cpu-features/deps/cpu_features/.dockerignore +31 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/Dockerfile +5 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/aarch64_linux_cmake.yml +30 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_freebsd_cmake.yml +22 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_linux_bazel.yml +26 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_linux_cmake.yml +31 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_macos_cmake.yml +43 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_windows_cmake.yml +25 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/arm_linux_cmake.yml +31 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/clang_format.yml +24 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/mips_linux_cmake.yml +30 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/power_linux_cmake.yml +29 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/riscv_linux_cmake.yml +28 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/s390x_linux_cmake.yml +27 -0
- package/node_modules/cpu-features/deps/cpu_features/.grenrc.yml +21 -0
- package/node_modules/cpu-features/deps/cpu_features/BUILD.bazel +329 -0
- package/node_modules/cpu-features/deps/cpu_features/CMakeLists.txt +261 -0
- package/node_modules/cpu-features/deps/cpu_features/CONTRIBUTING.md +23 -0
- package/node_modules/cpu-features/deps/cpu_features/LICENSE +230 -0
- package/node_modules/cpu-features/deps/cpu_features/README.md +272 -0
- package/node_modules/cpu-features/deps/cpu_features/WORKSPACE +19 -0
- package/node_modules/cpu-features/deps/cpu_features/bazel/ci/README.md +5 -0
- package/node_modules/cpu-features/deps/cpu_features/bazel/platforms.bzl +11 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesConfig.cmake.in +3 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesNdkCompatConfig.cmake.in +3 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/README.md +30 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/Makefile +252 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/README.md +40 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.dot +64 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.svg +312 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/generate_image.sh +7 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/amd64/Dockerfile +48 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/toolchain/Dockerfile +34 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/CMakeLists.txt +22 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/main.cpp +11 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/vagrant/freebsd/Vagrantfile +107 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/googletest.CMakeLists.txt.in +15 -0
- package/node_modules/cpu-features/deps/cpu_features/cpu_features.gyp +105 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpu_features_cache_info.h +54 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpu_features_macros.h +384 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_aarch64.h +259 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_arm.h +121 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_mips.h +74 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_ppc.h +149 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_riscv.h +72 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_s390x.h +108 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_x86.h +288 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/bit_utils.h +40 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/cpuid_x86.h +37 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/filesystem.h +39 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/hwcaps.h +240 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/stack_line_reader.h +49 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/string_view.h +110 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/windows_utils.h +70 -0
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/CMakeLists.txt +60 -0
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/README.md +4 -0
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.c +205 -0
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.h +320 -0
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/ndk-compat-test.c +12 -0
- package/node_modules/cpu-features/deps/cpu_features/patches/0001-Add-Apple-Silicon-Support.patch +456 -0
- package/node_modules/cpu-features/deps/cpu_features/scripts/generate_badges.d +165 -0
- package/node_modules/cpu-features/deps/cpu_features/scripts/make_release.sh +75 -0
- package/node_modules/cpu-features/deps/cpu_features/scripts/run_integration.sh +490 -0
- package/node_modules/cpu-features/deps/cpu_features/scripts/test_integration.sh +79 -0
- package/node_modules/cpu-features/deps/cpu_features/src/copy.inl +19 -0
- package/node_modules/cpu-features/deps/cpu_features/src/define_introspection.inl +86 -0
- package/node_modules/cpu-features/deps/cpu_features/src/define_introspection_and_hwcaps.inl +26 -0
- package/node_modules/cpu-features/deps/cpu_features/src/equals.inl +22 -0
- package/node_modules/cpu-features/deps/cpu_features/src/filesystem.c +62 -0
- package/node_modules/cpu-features/deps/cpu_features/src/hwcaps.c +169 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64__base_implementation.inl +88 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_linux_or_android.c +79 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_macos_or_iphone.c +82 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_windows.c +138 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_arm_linux_or_android.c +212 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_mips_linux_or_android.c +93 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_ppc_linux.c +163 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_riscv_linux.c +111 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_s390x_linux.c +120 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86__base_implementation.inl +2055 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_freebsd.c +68 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_linux_or_android.c +58 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_macos.c +57 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_windows.c +58 -0
- package/node_modules/cpu-features/deps/cpu_features/src/stack_line_reader.c +132 -0
- package/node_modules/cpu-features/deps/cpu_features/src/string_view.c +192 -0
- package/node_modules/cpu-features/deps/cpu_features/src/utils/list_cpu_features.c +461 -0
- package/node_modules/cpu-features/deps/cpu_features/test/CMakeLists.txt +114 -0
- package/node_modules/cpu-features/deps/cpu_features/test/bit_utils_test.cc +53 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_aarch64_test.cc +396 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_arm_test.cc +384 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_mips_test.cc +176 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_ppc_test.cc +131 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_riscv_test.cc +180 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_s390x_test.cc +82 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_x86_test.cc +1729 -0
- package/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.cc +103 -0
- package/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.h +61 -0
- package/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.cc +52 -0
- package/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.h +31 -0
- package/node_modules/cpu-features/deps/cpu_features/test/stack_line_reader_test.cc +132 -0
- package/node_modules/cpu-features/deps/cpu_features/test/string_view_test.cc +202 -0
- package/node_modules/cpu-features/lib/index.js +5 -0
- package/node_modules/cpu-features/package.json +40 -0
- package/node_modules/cpu-features/src/binding.cc +151 -0
- package/node_modules/cpu-features/test/test.js +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// dsh-remote — remote instruction-file index (issue #68).
|
|
2
|
+
//
|
|
3
|
+
// The core instruction loader (@deepseek-ai/dsh-agent-instructions) discovers
|
|
4
|
+
// AGENTS.md through the LOCAL filesystem only: from two user-global roots plus
|
|
5
|
+
// the projectRoot→cwd ancestor chain. A remote session's cwd is the local
|
|
6
|
+
// MIRROR of the remote workspace, and that mirror stays empty until rw_sync —
|
|
7
|
+
// so a 20 KB AGENTS.md that governs the remote repo never enters the model's
|
|
8
|
+
// context, while the same file is auto-injected in a local session. File
|
|
9
|
+
// ACCESS was never the problem (rw_read_file reads it fine); what is missing
|
|
10
|
+
// is the model knowing it should.
|
|
11
|
+
//
|
|
12
|
+
// This module closes the gap on the plugin side, along the two cheapest axes
|
|
13
|
+
// the issue lists (the third — a remote ctx.fs provider — belongs to core):
|
|
14
|
+
//
|
|
15
|
+
// 1. INDEX in the system prompt: a shallow scan of the remote workspace
|
|
16
|
+
// (root + at most two directory levels down, AGENTS.md / CLAUDE.md only)
|
|
17
|
+
// is rendered into the dsh-remote prompt section as a path list with
|
|
18
|
+
// headings, telling every new session the files exist and to read them
|
|
19
|
+
// with rw_read_file before editing that tree. This is an INDEX, not a
|
|
20
|
+
// second injection pipeline — the full text stays out of the prompt and
|
|
21
|
+
// out of any parallel baseline, exactly as the issue demands.
|
|
22
|
+
// 2. MIRROR the found files into the local mirror at their relative paths
|
|
23
|
+
// (three-way safe — a local edit is never clobbered, see below). A
|
|
24
|
+
// root-level AGENTS.md then sits exactly where the CORE loader looks in
|
|
25
|
+
// a local session, so the normal baseline/incremental machinery picks it
|
|
26
|
+
// up unchanged; no plugin-side injection logic runs in parallel with it.
|
|
27
|
+
//
|
|
28
|
+
// Deliberate non-goals (the issue's "请不要做"):
|
|
29
|
+
// - NO unbounded recursion of the remote root (multi-project roots like
|
|
30
|
+
// /root would scan forever): the walk is depth- and count-capped.
|
|
31
|
+
// - NO full-text injection from the plugin.
|
|
32
|
+
|
|
33
|
+
import { existsSync, mkdirSync, statSync, writeFileSync, utimesSync } from 'node:fs'
|
|
34
|
+
import path from 'node:path'
|
|
35
|
+
import { joinRemotePath, relPathUnder } from './paths.js'
|
|
36
|
+
import { loadSyncState, saveSyncState } from './sync.js'
|
|
37
|
+
|
|
38
|
+
/** File names treated as agent instruction files (compared case-insensitively;
|
|
39
|
+
* the mirrored copy keeps the remote's exact casing). */
|
|
40
|
+
export const INSTRUCTION_NAMES = ['agents.md', 'claude.md']
|
|
41
|
+
|
|
42
|
+
/** An instruction file bigger than this is indexed (path + note) but not read. */
|
|
43
|
+
export const INSTRUCTION_MAX_BYTES = 256 * 1024
|
|
44
|
+
|
|
45
|
+
/** Caps that keep the shallow scan shallow. */
|
|
46
|
+
export const INSTRUCTION_MAX_FILES = 20
|
|
47
|
+
export const INSTRUCTION_MAX_DIRS = 60
|
|
48
|
+
export const INSTRUCTION_MAX_DEPTH = 2 // 0 = workspace root, 1 = sub/, 2 = sub/sub/
|
|
49
|
+
|
|
50
|
+
/** Directories that never contain a project's governing instructions. */
|
|
51
|
+
const SKIP_DIRS = new Set([
|
|
52
|
+
'node_modules', '.git', '.svn', '.hg', 'dist', 'build', 'out',
|
|
53
|
+
'.next', '.nuxt', '.cache', '.turbo', 'coverage', 'target', 'vendor',
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
export const isInstructionName = (name) => INSTRUCTION_NAMES.includes(String(name || '').toLowerCase())
|
|
57
|
+
|
|
58
|
+
/** Pull the first Markdown headings out of an instruction file, for the index. */
|
|
59
|
+
export function extractHeadings(text, max = 8) {
|
|
60
|
+
const out = []
|
|
61
|
+
for (const line of String(text || '').split('\n')) {
|
|
62
|
+
const m = /^#{1,3}\s+(.+?)\s*#*\s*$/.exec(line)
|
|
63
|
+
if (!m) continue
|
|
64
|
+
out.push(m[1].slice(0, 60))
|
|
65
|
+
if (out.length >= max) break
|
|
66
|
+
}
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Shallow-scan a remote workspace for instruction files: the root directory
|
|
72
|
+
* plus up to INSTRUCTION_MAX_DEPTH levels of subdirectories, matching
|
|
73
|
+
* AGENTS.md / CLAUDE.md basenames. Bounded by INSTRUCTION_MAX_DIRS readdirs
|
|
74
|
+
* and INSTRUCTION_MAX_FILES files; anything past the caps is silently
|
|
75
|
+
* skipped (the index is a hint, not an inventory).
|
|
76
|
+
*
|
|
77
|
+
* @param {object} sftp - the promisified sftp surface (readdir/stat/readFile).
|
|
78
|
+
* @param {string} remoteRoot - absolute remote workspace path (normalized).
|
|
79
|
+
* @returns {Promise<Array<{rel: string, remote: string, size: number, mtime: number, content: Buffer|null, headings: string[]}>>}
|
|
80
|
+
* `content`/`headings` are null/empty for files over INSTRUCTION_MAX_BYTES.
|
|
81
|
+
*/
|
|
82
|
+
export async function scanInstructionFiles(sftp, remoteRoot, opts = {}) {
|
|
83
|
+
const maxDepth = Math.max(0, Math.min(Number(opts.maxDepth ?? INSTRUCTION_MAX_DEPTH) || 0, 4))
|
|
84
|
+
const maxDirs = Math.max(1, Number(opts.maxDirs ?? INSTRUCTION_MAX_DIRS) || INSTRUCTION_MAX_DIRS)
|
|
85
|
+
const maxFiles = Math.max(1, Number(opts.maxFiles ?? INSTRUCTION_MAX_FILES) || INSTRUCTION_MAX_FILES)
|
|
86
|
+
const files = []
|
|
87
|
+
// Breadth-first so a wide root cannot push the walk deep before the shallow
|
|
88
|
+
// (more commonly governing) files are seen.
|
|
89
|
+
const queue = [{ dir: remoteRoot, depth: 0 }]
|
|
90
|
+
let dirsScanned = 0
|
|
91
|
+
while (queue.length && files.length < maxFiles && dirsScanned < maxDirs) {
|
|
92
|
+
const { dir, depth } = queue.shift()
|
|
93
|
+
dirsScanned++
|
|
94
|
+
let entries = []
|
|
95
|
+
try { entries = (await sftp.readdir(dir)) || [] } catch { continue }
|
|
96
|
+
for (const e of entries) {
|
|
97
|
+
const name = String(e.filename || '')
|
|
98
|
+
if (!name || name === '.' || name === '..' || name.startsWith('.')) {
|
|
99
|
+
// Hidden entries are skipped wholesale EXCEPT the names themselves
|
|
100
|
+
// never start with a dot, so nothing is lost.
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
const isDir = !!(e.attrs && typeof e.attrs.isDirectory === 'function' && e.attrs.isDirectory())
|
|
104
|
+
const isLink = !!(e.attrs && typeof e.attrs.isSymbolicLink === 'function' && e.attrs.isSymbolicLink())
|
|
105
|
+
if (isDir) {
|
|
106
|
+
// Symlinked dirs are not followed: the walk stays inside the workspace
|
|
107
|
+
// tree and cannot loop through a link back to an ancestor.
|
|
108
|
+
if (isLink) continue
|
|
109
|
+
if (depth < maxDepth && !SKIP_DIRS.has(name.toLowerCase()) && queue.length + dirsScanned < maxDirs) {
|
|
110
|
+
queue.push({ dir: joinRemotePath(dir, name), depth: depth + 1 })
|
|
111
|
+
}
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
if (!isInstructionName(name)) continue
|
|
115
|
+
const remote = joinRemotePath(dir, name)
|
|
116
|
+
const rel = relPathUnder(remoteRoot, remote) || name
|
|
117
|
+
let st = null
|
|
118
|
+
try { st = await sftp.stat(remote) } catch { continue }
|
|
119
|
+
const size = st && typeof st.size === 'number' ? st.size : 0
|
|
120
|
+
const mtime = st && typeof st.mtime === 'number' ? st.mtime : 0
|
|
121
|
+
let content = null
|
|
122
|
+
let headings = []
|
|
123
|
+
if (size <= INSTRUCTION_MAX_BYTES) {
|
|
124
|
+
try {
|
|
125
|
+
content = await sftp.readFile(remote)
|
|
126
|
+
headings = extractHeadings(content.toString('utf8'))
|
|
127
|
+
} catch { content = null }
|
|
128
|
+
}
|
|
129
|
+
files.push({ rel, remote, size, mtime, content, headings })
|
|
130
|
+
if (files.length >= maxFiles) break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return files
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The three-way keys mirror lib/sync.js exactly (size + whole-second mtime):
|
|
137
|
+
// a file this module places into the mirror must be indistinguishable from
|
|
138
|
+
// one rw_sync pulled, or the next sync would report a phantom conflict.
|
|
139
|
+
const keyOf = (size, mtime) => `${size}:${Math.floor(mtime || 0)}`
|
|
140
|
+
const localKeyOf = (st) => (st ? keyOf(st.size, st.mtimeMs / 1000) : null)
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Write scanned instruction files into the local mirror at their workspace-
|
|
144
|
+
* relative paths, three-way safe against `.dsh-remote-sync-state.json`:
|
|
145
|
+
*
|
|
146
|
+
* - local missing → write (+ align mtime, record state)
|
|
147
|
+
* - local identical to remote → keep (record state if absent)
|
|
148
|
+
* - remote changed, local untouched (L==S) → write (a plain pull)
|
|
149
|
+
* - local edited (L!=S), remote unchanged → SKIP — a local edit is never
|
|
150
|
+
* clobbered; rw_sync surfaces it as a conflict like any other file
|
|
151
|
+
* - both changed / unknown local content → SKIP (rw_sync arbitrates)
|
|
152
|
+
*
|
|
153
|
+
* @returns {{written: string[], present: Set<string>}} rel paths written now,
|
|
154
|
+
* and every rel that exists locally afterwards (for the index's "mirrored"
|
|
155
|
+
* badge).
|
|
156
|
+
*/
|
|
157
|
+
export function syncInstructionsToMirror(files, mirrorDir) {
|
|
158
|
+
const written = []
|
|
159
|
+
const present = new Set()
|
|
160
|
+
if (!mirrorDir || !Array.isArray(files) || !files.length) return { written, present }
|
|
161
|
+
const state = loadSyncState(mirrorDir)
|
|
162
|
+
let stateDirty = false
|
|
163
|
+
for (const f of files) {
|
|
164
|
+
if (!f || !f.rel || !f.content) continue
|
|
165
|
+
const lp = path.join(mirrorDir, ...String(f.rel).split('/'))
|
|
166
|
+
const rk = keyOf(f.size, f.mtime)
|
|
167
|
+
const sk = state[f.rel] ? keyOf(state[f.rel].size, state[f.rel].mtime) : null
|
|
168
|
+
let lk = null
|
|
169
|
+
try { lk = existsSync(lp) ? localKeyOf(statSync(lp)) : null } catch { lk = null }
|
|
170
|
+
if (lk === rk) {
|
|
171
|
+
// Identical bytes in place already (same size AND mtime second) — only
|
|
172
|
+
// make sure the snapshot knows about it.
|
|
173
|
+
present.add(f.rel)
|
|
174
|
+
if (!sk) { state[f.rel] = { size: f.size, mtime: f.mtime }; stateDirty = true }
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
const canWrite = lk === null || (sk !== null && rk !== sk && lk === sk)
|
|
178
|
+
if (!canWrite) continue
|
|
179
|
+
try {
|
|
180
|
+
mkdirSync(path.dirname(lp), { recursive: true })
|
|
181
|
+
writeFileSync(lp, f.content)
|
|
182
|
+
// Align the local mtime to the remote's so the next rw_sync sees
|
|
183
|
+
// L==R==S and skips the file instead of flagging a local modification.
|
|
184
|
+
try { utimesSync(lp, new Date(f.mtime * 1000), new Date(f.mtime * 1000)) } catch { /* cosmetic */ }
|
|
185
|
+
state[f.rel] = { size: f.size, mtime: f.mtime }
|
|
186
|
+
stateDirty = true
|
|
187
|
+
written.push(f.rel)
|
|
188
|
+
present.add(f.rel)
|
|
189
|
+
} catch { /* a mirror write failure must not break the index */ }
|
|
190
|
+
}
|
|
191
|
+
if (stateDirty) {
|
|
192
|
+
try { saveSyncState(mirrorDir, state) } catch { /* best effort */ }
|
|
193
|
+
}
|
|
194
|
+
return { written, present }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Render the prompt-section index of remote instruction files. Paths are
|
|
199
|
+
* shown through `display` (the platform-aware display formatter), headings
|
|
200
|
+
* give the model a content hint without injecting the full text.
|
|
201
|
+
*/
|
|
202
|
+
export function formatInstructionIndex(files, display = (p) => p) {
|
|
203
|
+
const lines = files.slice(0, 10).map((f) => {
|
|
204
|
+
const h = f.headings && f.headings.length ? ` (headings: ${f.headings.slice(0, 6).join(' / ')})` : ''
|
|
205
|
+
const over = f.size > INSTRUCTION_MAX_BYTES ? ` (${Math.round(f.size / 1024)} KB)` : ''
|
|
206
|
+
const mirrored = f.mirrored ? '; synced into the local mirror' : ''
|
|
207
|
+
return `- ${display(f.remote)}${h}${over}${mirrored}`
|
|
208
|
+
})
|
|
209
|
+
return (
|
|
210
|
+
'Remote instruction files found in this workspace (they govern how to work in their directories; '
|
|
211
|
+
+ 'root-level copies were synced into the local mirror, but nested ones are NOT auto-injected — '
|
|
212
|
+
+ 'read them with rw_read_file before editing that tree):\n'
|
|
213
|
+
+ lines.join('\n')
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Per-process index of remote instruction scans, keyed by mirror directory.
|
|
219
|
+
* Deduplicates in-flight scans, negative-caches failures for a minute (an
|
|
220
|
+
* offline machine must not turn every prompt assembly into a connect
|
|
221
|
+
* attempt), and never throws — the index is strictly best-effort.
|
|
222
|
+
*/
|
|
223
|
+
export function createInstructionIndex() {
|
|
224
|
+
const cache = new Map() // resolvedMirrorDir → { scannedAt, files } | { failedAt }
|
|
225
|
+
const inflight = new Map() // resolvedMirrorDir → Promise
|
|
226
|
+
const RETRY_AFTER_MS = 60 * 1000
|
|
227
|
+
|
|
228
|
+
const keyOfDir = (mirrorDir) => {
|
|
229
|
+
try { return path.resolve(String(mirrorDir)) } catch { return String(mirrorDir || '') }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Scan (or return the cached scan of) one mirror's remote workspace.
|
|
234
|
+
* @param {{pool: {sftp: Function}, ws: string, mirrorDir: string, force?: boolean}} arg
|
|
235
|
+
* @returns {Promise<{scannedAt: number, files: Array}|null>}
|
|
236
|
+
*/
|
|
237
|
+
const refresh = ({ pool, ws, mirrorDir, force = false }) => {
|
|
238
|
+
if (!pool || typeof pool.sftp !== 'function' || !ws || !mirrorDir) return Promise.resolve(null)
|
|
239
|
+
const key = keyOfDir(mirrorDir)
|
|
240
|
+
const cur = cache.get(key)
|
|
241
|
+
if (!force && cur && cur.files) return Promise.resolve(cur)
|
|
242
|
+
if (!force && cur && cur.failedAt && Date.now() - cur.failedAt < RETRY_AFTER_MS) return Promise.resolve(null)
|
|
243
|
+
if (inflight.has(key)) return inflight.get(key)
|
|
244
|
+
const job = (async () => {
|
|
245
|
+
try {
|
|
246
|
+
const sftp = await pool.sftp()
|
|
247
|
+
const files = await scanInstructionFiles(sftp, ws)
|
|
248
|
+
const synced = syncInstructionsToMirror(files, mirrorDir)
|
|
249
|
+
const entry = {
|
|
250
|
+
scannedAt: Date.now(),
|
|
251
|
+
files: files.map((f) => ({
|
|
252
|
+
rel: f.rel,
|
|
253
|
+
remote: f.remote,
|
|
254
|
+
size: f.size,
|
|
255
|
+
headings: f.headings,
|
|
256
|
+
mirrored: synced.present.has(f.rel),
|
|
257
|
+
})),
|
|
258
|
+
}
|
|
259
|
+
cache.set(key, entry)
|
|
260
|
+
return entry
|
|
261
|
+
} catch {
|
|
262
|
+
// Negative-cache: the machine may simply be offline; retry at most
|
|
263
|
+
// once a minute, not once per prompt assembly.
|
|
264
|
+
cache.set(key, { failedAt: Date.now() })
|
|
265
|
+
return null
|
|
266
|
+
} finally {
|
|
267
|
+
inflight.delete(key)
|
|
268
|
+
}
|
|
269
|
+
})()
|
|
270
|
+
inflight.set(key, job)
|
|
271
|
+
return job
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The cached index for a mirror, or null when unknown/failed. */
|
|
275
|
+
const get = (mirrorDir) => {
|
|
276
|
+
const e = cache.get(keyOfDir(mirrorDir))
|
|
277
|
+
return e && e.files ? e : null
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Fire-and-forget refresh for prompt-assembly time: starts a scan only
|
|
281
|
+
* when nothing is known about the mirror yet (recent failures included). */
|
|
282
|
+
const note = ({ pool, ws, mirrorDir }) => {
|
|
283
|
+
if (cache.has(keyOfDir(mirrorDir))) return
|
|
284
|
+
refresh({ pool, ws, mirrorDir }).catch(() => { /* guarded inside */ })
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { refresh, get, note, _cache: cache }
|
|
288
|
+
}
|
package/lib/openssh.js
CHANGED
|
@@ -785,7 +785,16 @@ export class SystemOpenSshPool {
|
|
|
785
785
|
throw new Error('ssh target changed while opening sftp')
|
|
786
786
|
}
|
|
787
787
|
this._sftpChild = child
|
|
788
|
-
return buildSftpSurface(sftp, {
|
|
788
|
+
return buildSftpSurface(sftp, {
|
|
789
|
+
commandTimeoutMs: this.config.commandTimeoutMs,
|
|
790
|
+
onTimeout: () => {
|
|
791
|
+
// A timed-out SFTP session is dead: kill the child and drop the cached
|
|
792
|
+
// session so the next call re-handshakes instead of queueing behind the
|
|
793
|
+
// wedged one (issue #65).
|
|
794
|
+
try { child.kill() } catch { /* already gone */ }
|
|
795
|
+
drop()
|
|
796
|
+
},
|
|
797
|
+
})
|
|
789
798
|
})
|
|
790
799
|
// Never cache a rejected open: the next call must retry rather than get the
|
|
791
800
|
// same error back forever (a failed handshake is usually transient).
|
package/lib/pool.js
CHANGED
|
@@ -197,6 +197,8 @@ export class SshPool {
|
|
|
197
197
|
this.onReady = null
|
|
198
198
|
/** Optional hook called when the pool closes. */
|
|
199
199
|
this.onCloseHook = null
|
|
200
|
+
/** Consecutive SFTP subchannel-open timeouts (issue #65). */
|
|
201
|
+
this._sftpTimeouts = 0
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
resolveKeyPath() {
|
|
@@ -324,6 +326,7 @@ export class SshPool {
|
|
|
324
326
|
return
|
|
325
327
|
}
|
|
326
328
|
this.client = client
|
|
329
|
+
this._sftpTimeouts = 0
|
|
327
330
|
resolve(client)
|
|
328
331
|
if (this.onReady) { try { this.onReady(client) } catch {} }
|
|
329
332
|
})
|
|
@@ -684,10 +687,25 @@ export class SshPool {
|
|
|
684
687
|
}
|
|
685
688
|
return reject(new Error('ssh sftp failed: ' + ((err && err.message) || err)))
|
|
686
689
|
}
|
|
690
|
+
// A successful open proves the connection healthy: the strike
|
|
691
|
+
// counter counts CONSECUTIVE timeouts, so it resets here —
|
|
692
|
+
// two unrelated transient blips hours apart must not tear down
|
|
693
|
+
// a fine connection.
|
|
694
|
+
this._sftpTimeouts = 0
|
|
687
695
|
// The promisified surface (timeouts, dead-channel fail-fast,
|
|
688
696
|
// range reads) is shared verbatim with the system-OpenSSH
|
|
689
697
|
// backend — see lib/sftp-surface.js for the rules it encodes.
|
|
690
|
-
resolve(buildSftpSurface(sftp, {
|
|
698
|
+
resolve(buildSftpSurface(sftp, {
|
|
699
|
+
commandTimeoutMs: this.config.commandTimeoutMs,
|
|
700
|
+
onTimeout: () => {
|
|
701
|
+
// A timed-out SFTP subchannel is dead: close it so the next
|
|
702
|
+
// operation does not queue behind the wedged one, and count
|
|
703
|
+
// the strike so a second consecutive timeout tears down the
|
|
704
|
+
// whole pooled connection (issue #65).
|
|
705
|
+
try { sftp.end() } catch { /* already gone */ }
|
|
706
|
+
this._noteSftpTimeout()
|
|
707
|
+
},
|
|
708
|
+
}))
|
|
691
709
|
})
|
|
692
710
|
} catch (e) {
|
|
693
711
|
clearAttempt()
|
|
@@ -699,6 +717,16 @@ export class SshPool {
|
|
|
699
717
|
)
|
|
700
718
|
}
|
|
701
719
|
|
|
720
|
+
/**
|
|
721
|
+
* Count a timed-out SFTP subchannel open. A second consecutive timeout means
|
|
722
|
+
* the pooled connection itself is wedged (issue #65): tear it down so the
|
|
723
|
+
* next sftp() call re-handshakes instead of queueing behind the dead one.
|
|
724
|
+
*/
|
|
725
|
+
_noteSftpTimeout() {
|
|
726
|
+
this._sftpTimeouts = (this._sftpTimeouts || 0) + 1
|
|
727
|
+
if (this._sftpTimeouts >= 2) this.invalidate()
|
|
728
|
+
}
|
|
729
|
+
|
|
702
730
|
/**
|
|
703
731
|
* Drop the cached client and force a fresh connection on the next call.
|
|
704
732
|
* Called when a channel open fails (e.g. "Channel open failure: open
|
package/lib/remote-fs.js
CHANGED
|
@@ -98,6 +98,17 @@ export async function removeRemoteTree(sftp, p, maxFiles = 2000) {
|
|
|
98
98
|
return removed
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/** MIME types for image formats the preview panel can render inline. */
|
|
102
|
+
const IMAGE_MIME_TYPES = {
|
|
103
|
+
png: 'image/png',
|
|
104
|
+
jpg: 'image/jpeg',
|
|
105
|
+
jpeg: 'image/jpeg',
|
|
106
|
+
gif: 'image/gif',
|
|
107
|
+
webp: 'image/webp',
|
|
108
|
+
svg: 'image/svg+xml',
|
|
109
|
+
bmp: 'image/bmp',
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
function looksBinary(buf) {
|
|
102
113
|
const n = Math.min(buf.length, 8192)
|
|
103
114
|
for (let i = 0; i < n; i++) if (buf[i] === 0) return true
|
|
@@ -107,6 +118,14 @@ function looksBinary(buf) {
|
|
|
107
118
|
/**
|
|
108
119
|
* Read a remote file for the sidebar/editor. Oversized files use a range read
|
|
109
120
|
* (`readPartial`) so we never `fastGet` the whole blob onto disk.
|
|
121
|
+
*
|
|
122
|
+
* Images are the exception (issue #66): a PREFIX of an image is a broken
|
|
123
|
+
* picture, not a preview, so for a recognised image extension the caller's
|
|
124
|
+
* text-oriented cap is skipped and the file is read whole up to the hard 2MB
|
|
125
|
+
* ceiling; the panel renders it inline as `<img src="data:…">`. An image past
|
|
126
|
+
* the ceiling degrades to the plain binary stub + download button rather than
|
|
127
|
+
* shipping a guaranteed-broken half render. (`<img>` is also the safe sink
|
|
128
|
+
* for SVG: unlike inline markup it cannot run the file's scripts.)
|
|
110
129
|
*/
|
|
111
130
|
export async function previewRemoteFile(sftp, target, opts = {}) {
|
|
112
131
|
const {
|
|
@@ -114,7 +133,10 @@ export async function previewRemoteFile(sftp, target, opts = {}) {
|
|
|
114
133
|
encoding = 'utf-8',
|
|
115
134
|
decodeBuf = (buf) => buf.toString('utf8'),
|
|
116
135
|
} = opts
|
|
117
|
-
const
|
|
136
|
+
const hardCap = 2 * 1024 * 1024
|
|
137
|
+
const name = String(target).split(/[\\/]/).pop() || ''
|
|
138
|
+
const imageMime = IMAGE_MIME_TYPES[(name.split('.').pop() || '').toLowerCase()]
|
|
139
|
+
const cap = imageMime ? hardCap : Math.min(Math.max(Number(maxBytes) || 256 * 1024, 1024), hardCap)
|
|
118
140
|
const st = await sftp.stat(target)
|
|
119
141
|
const mtime = st && typeof st.mtime === 'number' ? st.mtime : 0
|
|
120
142
|
const size = st && typeof st.size === 'number' ? st.size : 0
|
|
@@ -131,6 +153,9 @@ export async function previewRemoteFile(sftp, target, opts = {}) {
|
|
|
131
153
|
} else {
|
|
132
154
|
buf = await sftp.readFile(target)
|
|
133
155
|
}
|
|
156
|
+
if (imageMime && !truncated) {
|
|
157
|
+
return { ok: true, binary: false, image: true, mime: imageMime, content: buf.toString('base64'), truncated, size, mtime }
|
|
158
|
+
}
|
|
134
159
|
if (looksBinary(buf)) {
|
|
135
160
|
return {
|
|
136
161
|
ok: true, binary: true, size, mtime,
|
package/lib/sftp-surface.js
CHANGED
|
@@ -26,7 +26,7 @@ import { toSftpPath } from './paths.js'
|
|
|
26
26
|
* @param {number} opts.commandTimeoutMs - per-operation timeout.
|
|
27
27
|
* @returns {object} promisified sftp surface.
|
|
28
28
|
*/
|
|
29
|
-
export function buildSftpSurface(sftp, { commandTimeoutMs }) {
|
|
29
|
+
export function buildSftpSurface(sftp, { commandTimeoutMs, onTimeout }) {
|
|
30
30
|
// A dead channel must fail fast (see the header). ssh2's SFTP instance and the
|
|
31
31
|
// system-OpenSSH engine both emit these, so one hook covers both backends.
|
|
32
32
|
let dead = false
|
|
@@ -39,12 +39,19 @@ export function buildSftpSurface(sftp, { commandTimeoutMs }) {
|
|
|
39
39
|
const withTimeout = (fn) => (...args) =>
|
|
40
40
|
new Promise((resolve, reject) => {
|
|
41
41
|
if (dead) return reject(new Error('sftp channel closed'))
|
|
42
|
-
|
|
42
|
+
let timedOut = false
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
timedOut = true
|
|
45
|
+
markDead()
|
|
46
|
+
if (typeof onTimeout === 'function') { try { onTimeout() } catch { /* best effort */ } }
|
|
47
|
+
reject(new Error('sftp operation timed out'))
|
|
48
|
+
}, commandTimeoutMs)
|
|
43
49
|
const finish = (err, value) => {
|
|
44
50
|
clearTimeout(timer)
|
|
51
|
+
if (timedOut) return
|
|
45
52
|
err ? reject(err) : resolve(value)
|
|
46
53
|
}
|
|
47
|
-
try { fn(...args, finish) } catch (err) { clearTimeout(timer); reject(err) }
|
|
54
|
+
try { fn(...args, finish) } catch (err) { clearTimeout(timer); if (!timedOut) reject(err) }
|
|
48
55
|
})
|
|
49
56
|
const P = (p) => toSftpPath(p)
|
|
50
57
|
return {
|
|
@@ -55,7 +62,43 @@ export function buildSftpSurface(sftp, { commandTimeoutMs }) {
|
|
|
55
62
|
rmdir: (dir) => withTimeout((d, cb) => sftp.rmdir(d, cb))(P(dir)),
|
|
56
63
|
unlink: (p) => withTimeout((d, cb) => sftp.unlink(d, cb))(P(p)),
|
|
57
64
|
rename: (p, d) => withTimeout((a, b, cb) => sftp.rename(a, b, cb))(P(p), P(d)),
|
|
65
|
+
// POSIX rename(2) semantics: atomically REPLACE an existing destination.
|
|
66
|
+
// Plain SSH_FXP_RENAME fails when the target exists (OpenSSH's sftp-server
|
|
67
|
+
// enforces that even though it advertises posix-rename@openssh.com — ssh2
|
|
68
|
+
// only sends the extension when asked explicitly, verified against
|
|
69
|
+
// OpenSSH_9.6 in issue #65). Servers with the extension get one atomic
|
|
70
|
+
// request; servers without it get the move-aside dance, whose every hop
|
|
71
|
+
// targets a path that does not exist, which plain RENAME always permits.
|
|
72
|
+
// rw_edit's atomic write depends on this — without it every edit of an
|
|
73
|
+
// existing file fails with a bare "Failure".
|
|
74
|
+
renameOverwrite: (p, d) => withTimeout((a, b, cb) => {
|
|
75
|
+
const ext = sftp._extensions && sftp._extensions['posix-rename@openssh.com']
|
|
76
|
+
if (ext === '1' && typeof sftp.ext_openssh_rename === 'function') {
|
|
77
|
+
return sftp.ext_openssh_rename(a, b, cb)
|
|
78
|
+
}
|
|
79
|
+
const bak = `${b}.dsh-remote-bak-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.tmp`
|
|
80
|
+
sftp.rename(b, bak, (e1) => {
|
|
81
|
+
if (e1) {
|
|
82
|
+
// Destination absent → a plain rename is already overwrite-safe.
|
|
83
|
+
if (/no such/i.test(String((e1 && e1.message) || e1))) return sftp.rename(a, b, cb)
|
|
84
|
+
return cb(e1)
|
|
85
|
+
}
|
|
86
|
+
sftp.rename(a, b, (e2) => {
|
|
87
|
+
if (e2) {
|
|
88
|
+
// Roll the original back into place before reporting the failure.
|
|
89
|
+
return sftp.rename(bak, b, () => cb(e2))
|
|
90
|
+
}
|
|
91
|
+
// Best effort: a leftover backup must not fail the operation.
|
|
92
|
+
sftp.unlink(bak, () => cb(null))
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
})(P(p), P(d)),
|
|
58
96
|
realpath: (p) => withTimeout((d, cb) => sftp.realpath(d, cb))(P(p)),
|
|
97
|
+
// Attribute write (chmod semantics live in setstat at the protocol level,
|
|
98
|
+
// so BOTH backends — ssh2's SFTPWrapper and its raw protocol SFTP — answer
|
|
99
|
+
// this one shape). Used by rw_edit to restore the original mode after an
|
|
100
|
+
// atomic rename replace (issue #65).
|
|
101
|
+
setstat: (p, attrs) => withTimeout((d, a, cb) => sftp.setstat(d, a, cb))(P(p), attrs),
|
|
59
102
|
readFile: (p) => withTimeout((d, cb) => sftp.readFile(d, cb))(P(p)),
|
|
60
103
|
writeFile: (p, data) => withTimeout((d, data2, cb) => sftp.writeFile(d, data2, cb))(P(p), data),
|
|
61
104
|
fastGet: (p, lp) => withTimeout((d, l, cb) => sftp.fastGet(d, l, cb))(P(p), lp),
|
package/lib/sshconfig.js
CHANGED
|
@@ -32,11 +32,21 @@ import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSy
|
|
|
32
32
|
import { homedir } from 'node:os'
|
|
33
33
|
import path from 'node:path'
|
|
34
34
|
|
|
35
|
-
/** Default location of the user's ssh config.
|
|
36
|
-
|
|
35
|
+
/** Default location of the user's ssh config. An env override exists so tests
|
|
36
|
+
* (and any deployment that isolates its home directory) can point the plugin
|
|
37
|
+
* at a substitute file instead of the real ~/.ssh/config. */
|
|
38
|
+
export const sshConfigPath = () => {
|
|
39
|
+
const env = process.env.DSH_REMOTE_SSH_CONFIG
|
|
40
|
+
if (env && String(env).trim()) return path.resolve(String(env).trim())
|
|
41
|
+
return path.join(homedir(), '.ssh', 'config')
|
|
42
|
+
}
|
|
37
43
|
|
|
38
44
|
/** Directory `Include` paths are resolved against (ssh_config(5)). */
|
|
39
|
-
export const sshConfigDir = () =>
|
|
45
|
+
export const sshConfigDir = () => {
|
|
46
|
+
const env = process.env.DSH_REMOTE_SSH_CONFIG
|
|
47
|
+
if (env && String(env).trim()) return path.dirname(path.resolve(String(env).trim()))
|
|
48
|
+
return path.join(homedir(), '.ssh')
|
|
49
|
+
}
|
|
40
50
|
|
|
41
51
|
/** Parameters the resolver cares about, lowercase (as parsed). */
|
|
42
52
|
const KNOWN_PARAMS = new Set([
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [ master ]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
tests-linux:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.x, 24.x]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
with:
|
|
18
|
+
persist-credentials: false
|
|
19
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
20
|
+
uses: actions/setup-node@v4
|
|
21
|
+
with:
|
|
22
|
+
node-version: ${{ matrix.node-version }}
|
|
23
|
+
- name: Check Node.js version
|
|
24
|
+
run: node -pe process.versions
|
|
25
|
+
- name: Install module
|
|
26
|
+
run: npm install
|
|
27
|
+
- name: Run tests
|
|
28
|
+
run: npm test
|
|
29
|
+
tests-macos-intel:
|
|
30
|
+
runs-on: macos-15-intel
|
|
31
|
+
strategy:
|
|
32
|
+
fail-fast: false
|
|
33
|
+
matrix:
|
|
34
|
+
node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.x, 24.x]
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
with:
|
|
38
|
+
persist-credentials: false
|
|
39
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
40
|
+
uses: actions/setup-node@v4
|
|
41
|
+
with:
|
|
42
|
+
node-version: ${{ matrix.node-version }}
|
|
43
|
+
- name: Check Node.js version
|
|
44
|
+
run: node -pe process.versions
|
|
45
|
+
- name: Install module
|
|
46
|
+
run: npm install
|
|
47
|
+
- name: Run tests
|
|
48
|
+
run: npm test
|
|
49
|
+
tests-macos-arm:
|
|
50
|
+
runs-on: macos-latest
|
|
51
|
+
strategy:
|
|
52
|
+
fail-fast: false
|
|
53
|
+
matrix:
|
|
54
|
+
node-version: [16.x, 18.x, 20.x, 22.x, 24.x]
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
with:
|
|
58
|
+
persist-credentials: false
|
|
59
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
60
|
+
uses: actions/setup-node@v4
|
|
61
|
+
with:
|
|
62
|
+
node-version: ${{ matrix.node-version }}
|
|
63
|
+
- name: Check Node.js version
|
|
64
|
+
run: node -pe process.versions
|
|
65
|
+
- name: Install module
|
|
66
|
+
run: npm install
|
|
67
|
+
- name: Run tests
|
|
68
|
+
run: npm test
|
|
69
|
+
tests-windows-2022:
|
|
70
|
+
runs-on: windows-2022
|
|
71
|
+
strategy:
|
|
72
|
+
fail-fast: false
|
|
73
|
+
matrix:
|
|
74
|
+
node-version: [24.x]
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v4
|
|
77
|
+
with:
|
|
78
|
+
persist-credentials: false
|
|
79
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
80
|
+
uses: actions/setup-node@v4
|
|
81
|
+
with:
|
|
82
|
+
node-version: ${{ matrix.node-version }}
|
|
83
|
+
- name: Check Node.js version
|
|
84
|
+
run: node -pe process.versions
|
|
85
|
+
- name: Install module
|
|
86
|
+
run: npm install
|
|
87
|
+
- name: Run tests
|
|
88
|
+
run: npm test
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: lint
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [ master ]
|
|
7
|
+
|
|
8
|
+
env:
|
|
9
|
+
NODE_VERSION: 22.x
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
lint-js:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
with:
|
|
17
|
+
persist-credentials: false
|
|
18
|
+
- name: Use Node.js ${{ env.NODE_VERSION }}
|
|
19
|
+
uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
22
|
+
- name: Check Node.js version
|
|
23
|
+
run: node -pe process.versions
|
|
24
|
+
- name: Install ESLint + ESLint configs/plugins
|
|
25
|
+
run: npm install --only=dev
|
|
26
|
+
- name: Lint files
|
|
27
|
+
run: npm run lint
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright Brian White. All rights reserved.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
|
5
|
+
deal in the Software without restriction, including without limitation the
|
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
18
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
19
|
+
IN THE SOFTWARE.
|