skillvault 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/credentials.js +65 -3
- package/dist/projects-registry.js +97 -17
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ import { resolveScope, resolveRoots, } from './scope.js';
|
|
|
24
24
|
import { loadCredentials, saveCredentials, loadProjectConfig, saveProjectConfig, } from './credentials.js';
|
|
25
25
|
import { registerProject, unregisterProject, getCurrentProject, listProjects, } from './projects-registry.js';
|
|
26
26
|
import { confirmInstall } from './prompts.js';
|
|
27
|
-
const VERSION = '0.11.
|
|
27
|
+
const VERSION = '0.11.2';
|
|
28
28
|
const HOME = process.env.HOME || process.env.USERPROFILE || '~';
|
|
29
29
|
const API_URL = process.env.SKILLVAULT_API_URL || 'https://api.getskillvault.com';
|
|
30
30
|
const CONFIG_DIR = join(HOME, '.skillvault');
|
package/dist/credentials.js
CHANGED
|
@@ -12,10 +12,63 @@
|
|
|
12
12
|
* <roots.configDir>/project.json (per install, project- or global-scoped)
|
|
13
13
|
* Active publishers in this specific install plus install metadata.
|
|
14
14
|
*/
|
|
15
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
15
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, openSync, closeSync, unlinkSync, } from 'node:fs';
|
|
16
16
|
import { join, dirname } from 'node:path';
|
|
17
17
|
import { homedir } from 'node:os';
|
|
18
18
|
import { credentialsPath } from './scope.js';
|
|
19
|
+
/**
|
|
20
|
+
* Run `fn` while holding an exclusive advisory lock on `<targetPath>.lock`.
|
|
21
|
+
* Same primitive as projects-registry.ts withFileLock — duplicated here so
|
|
22
|
+
* credentials.ts can be locked without crossing module boundaries. See that
|
|
23
|
+
* file for the design notes (POSIX O_EXCL semantics, busy-wait backoff,
|
|
24
|
+
* 1s stale-lock reclaim).
|
|
25
|
+
*/
|
|
26
|
+
function withFileLock(targetPath, fn) {
|
|
27
|
+
const lockPath = targetPath + '.lock';
|
|
28
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
29
|
+
const start = Date.now();
|
|
30
|
+
const TIMEOUT_MS = 1000;
|
|
31
|
+
let fd = null;
|
|
32
|
+
while (fd === null) {
|
|
33
|
+
try {
|
|
34
|
+
fd = openSync(lockPath, 'wx', 0o600);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const code = err.code;
|
|
38
|
+
if (code !== 'EEXIST')
|
|
39
|
+
throw err;
|
|
40
|
+
if (Date.now() - start > TIMEOUT_MS) {
|
|
41
|
+
try {
|
|
42
|
+
unlinkSync(lockPath);
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
try {
|
|
46
|
+
fd = openSync(lockPath, 'wx', 0o600);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new Error(`Could not acquire lock at ${lockPath} after ${TIMEOUT_MS}ms`);
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
const buf = new SharedArrayBuffer(4);
|
|
54
|
+
const view = new Int32Array(buf);
|
|
55
|
+
Atomics.wait(view, 0, 0, 5 + Math.floor(Math.random() * 10));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return fn();
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
try {
|
|
63
|
+
closeSync(fd);
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
try {
|
|
67
|
+
unlinkSync(lockPath);
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
19
72
|
/**
|
|
20
73
|
* Load customer credentials from ~/.skillvault/credentials.json.
|
|
21
74
|
*
|
|
@@ -75,11 +128,20 @@ export function loadCredentials() {
|
|
|
75
128
|
}
|
|
76
129
|
/**
|
|
77
130
|
* Persist customer credentials to ~/.skillvault/credentials.json with mode 0600.
|
|
131
|
+
*
|
|
132
|
+
* Wrapped in withFileLock so two concurrent invites with different invite
|
|
133
|
+
* codes can't race and silently drop one of each other's publishers from
|
|
134
|
+
* the publishers[] array. The caller is expected to read-modify-write the
|
|
135
|
+
* full credentials object themselves; this function only serializes the
|
|
136
|
+
* write step. For full read-modify-write atomicity, callers should hold
|
|
137
|
+
* the lock across both load + save (see redeemInvite() in cli.ts).
|
|
78
138
|
*/
|
|
79
139
|
export function saveCredentials(creds) {
|
|
80
140
|
const path = credentialsPath();
|
|
81
|
-
|
|
82
|
-
|
|
141
|
+
withFileLock(path, () => {
|
|
142
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
143
|
+
writeFileSync(path, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
144
|
+
});
|
|
83
145
|
}
|
|
84
146
|
function projectConfigPath(roots) {
|
|
85
147
|
return join(roots.configDir, 'project.json');
|
|
@@ -6,10 +6,80 @@
|
|
|
6
6
|
* `getCurrentProject()` to figure out whether the current CWD already has a
|
|
7
7
|
* registered SkillVault install.
|
|
8
8
|
*/
|
|
9
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync, statSync } from 'node:fs';
|
|
9
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, statSync, openSync, closeSync, unlinkSync, } from 'node:fs';
|
|
10
10
|
import { dirname, resolve } from 'node:path';
|
|
11
11
|
import { projectsRegistryPath } from './scope.js';
|
|
12
12
|
const REGISTRY_VERSION = 1;
|
|
13
|
+
/**
|
|
14
|
+
* Run `fn` while holding an exclusive advisory lock on `<targetPath>.lock`.
|
|
15
|
+
*
|
|
16
|
+
* Uses `openSync(..., 'wx')` to atomically create the lockfile (POSIX
|
|
17
|
+
* O_EXCL semantics — exactly one process succeeds, the rest get EEXIST).
|
|
18
|
+
* On contention, busy-waits with a small backoff up to ~1s before giving
|
|
19
|
+
* up. The lockfile is unlinked on completion (success or thrown error)
|
|
20
|
+
* via try/finally so a crash mid-operation can't permanently wedge
|
|
21
|
+
* subsequent invocations — though if a process is SIGKILL'd between the
|
|
22
|
+
* openSync and the closeSync, the lockfile will linger and the next
|
|
23
|
+
* caller will time out and either inherit the stale lock (forced) or
|
|
24
|
+
* throw. For our use case (a single-user CLI doing read-modify-write on
|
|
25
|
+
* a small JSON file), 1s is plenty.
|
|
26
|
+
*
|
|
27
|
+
* Used by registerProject + unregisterProject to fix the lost-update race
|
|
28
|
+
* when two `--invite` commands run concurrently against the same
|
|
29
|
+
* ~/.skillvault/projects.json (audit scenario 12-concurrent-install).
|
|
30
|
+
*/
|
|
31
|
+
function withFileLock(targetPath, fn) {
|
|
32
|
+
const lockPath = targetPath + '.lock';
|
|
33
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
const TIMEOUT_MS = 1000;
|
|
36
|
+
let fd = null;
|
|
37
|
+
while (fd === null) {
|
|
38
|
+
try {
|
|
39
|
+
fd = openSync(lockPath, 'wx', 0o600);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const code = err.code;
|
|
43
|
+
if (code !== 'EEXIST')
|
|
44
|
+
throw err;
|
|
45
|
+
if (Date.now() - start > TIMEOUT_MS) {
|
|
46
|
+
// Stale lock — assume the previous holder crashed and reclaim it.
|
|
47
|
+
try {
|
|
48
|
+
unlinkSync(lockPath);
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
// Loop one more time, but if it still fails, throw.
|
|
52
|
+
try {
|
|
53
|
+
fd = openSync(lockPath, 'wx', 0o600);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error(`Could not acquire lock at ${lockPath} after ${TIMEOUT_MS}ms`);
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
// Busy-wait briefly. The Node.js event loop is single-threaded so
|
|
61
|
+
// we can't yield to a peer process via Promise without making the
|
|
62
|
+
// function async — and registerProject is sync. Atomics.wait on a
|
|
63
|
+
// SharedArrayBuffer is the cleanest sync sleep:
|
|
64
|
+
const buf = new SharedArrayBuffer(4);
|
|
65
|
+
const view = new Int32Array(buf);
|
|
66
|
+
Atomics.wait(view, 0, 0, 5 + Math.floor(Math.random() * 10));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return fn();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
try {
|
|
74
|
+
closeSync(fd);
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
try {
|
|
78
|
+
unlinkSync(lockPath);
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
13
83
|
function readRegistry() {
|
|
14
84
|
const path = projectsRegistryPath();
|
|
15
85
|
if (!existsSync(path))
|
|
@@ -75,29 +145,39 @@ export function listProjects() {
|
|
|
75
145
|
/**
|
|
76
146
|
* Insert or update a project entry. Matching is by absolute path; if an entry
|
|
77
147
|
* with the same path already exists, it is replaced.
|
|
148
|
+
*
|
|
149
|
+
* The read-modify-write is wrapped in withFileLock so two concurrent
|
|
150
|
+
* `--invite` calls can't race and lose one of each other's updates.
|
|
78
151
|
*/
|
|
79
152
|
export function registerProject(entry) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
153
|
+
withFileLock(projectsRegistryPath(), () => {
|
|
154
|
+
const reg = readRegistry();
|
|
155
|
+
const normalized = { ...entry, path: resolve(entry.path) };
|
|
156
|
+
const idx = reg.projects.findIndex((p) => resolve(p.path) === normalized.path);
|
|
157
|
+
if (idx >= 0) {
|
|
158
|
+
reg.projects[idx] = normalized;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
reg.projects.push(normalized);
|
|
162
|
+
}
|
|
163
|
+
writeRegistry(reg);
|
|
164
|
+
});
|
|
90
165
|
}
|
|
91
166
|
/**
|
|
92
167
|
* Remove the project entry whose path matches. No-op if not present.
|
|
168
|
+
*
|
|
169
|
+
* Wrapped in withFileLock for the same reason as registerProject — a
|
|
170
|
+
* concurrent uninstall + register would otherwise race.
|
|
93
171
|
*/
|
|
94
172
|
export function unregisterProject(path) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
173
|
+
withFileLock(projectsRegistryPath(), () => {
|
|
174
|
+
const reg = readRegistry();
|
|
175
|
+
const target = resolve(path);
|
|
176
|
+
const before = reg.projects.length;
|
|
177
|
+
reg.projects = reg.projects.filter((p) => resolve(p.path) !== target);
|
|
178
|
+
if (reg.projects.length !== before)
|
|
179
|
+
writeRegistry(reg);
|
|
180
|
+
});
|
|
101
181
|
}
|
|
102
182
|
/**
|
|
103
183
|
* Look up the registry entry that matches the current working directory (or
|