skillvault 0.9.3 → 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/dist/cli.js +1116 -197
- package/dist/credentials.js +108 -0
- package/dist/projects-registry.js +111 -0
- package/dist/prompts.js +193 -0
- package/dist/scope.js +99 -0
- package/package.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer credentials and per-install project config storage for the
|
|
3
|
+
* customer-facing agent CLI.
|
|
4
|
+
*
|
|
5
|
+
* Two files, two concerns:
|
|
6
|
+
*
|
|
7
|
+
* ~/.skillvault/credentials.json (always global)
|
|
8
|
+
* Customer identity — JWT, email, every publisher this customer can
|
|
9
|
+
* access. Never project-scoped, even when the install lives in a
|
|
10
|
+
* project directory.
|
|
11
|
+
*
|
|
12
|
+
* <roots.configDir>/project.json (per install, project- or global-scoped)
|
|
13
|
+
* Active publishers in this specific install plus install metadata.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
16
|
+
import { join, dirname } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { credentialsPath } from './scope.js';
|
|
19
|
+
/**
|
|
20
|
+
* Load customer credentials from ~/.skillvault/credentials.json.
|
|
21
|
+
*
|
|
22
|
+
* Falls back to a one-time migration from the legacy
|
|
23
|
+
* ~/.skillvault/agent-config.json file (the v0.9.x storage layout).
|
|
24
|
+
* On a successful migration the new credentials.json file is written
|
|
25
|
+
* but the legacy file is left in place so a downgrade still works.
|
|
26
|
+
*
|
|
27
|
+
* Returns null if neither file exists.
|
|
28
|
+
*/
|
|
29
|
+
export function loadCredentials() {
|
|
30
|
+
const path = credentialsPath();
|
|
31
|
+
try {
|
|
32
|
+
if (existsSync(path)) {
|
|
33
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// fall through to legacy reader
|
|
38
|
+
}
|
|
39
|
+
// Legacy fallback: read the old agent-config.json shape and convert.
|
|
40
|
+
const legacyPath = join(homedir(), '.skillvault', 'agent-config.json');
|
|
41
|
+
if (!existsSync(legacyPath))
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
const raw = JSON.parse(readFileSync(legacyPath, 'utf8'));
|
|
45
|
+
let publishers;
|
|
46
|
+
if (raw.token && !raw.publishers) {
|
|
47
|
+
// Pre-multi-publisher schema: a single token + publisher_id pair.
|
|
48
|
+
publishers = [{
|
|
49
|
+
id: raw.publisher_id,
|
|
50
|
+
name: raw.publisher_id,
|
|
51
|
+
token: raw.token,
|
|
52
|
+
added_at: raw.setup_at || new Date().toISOString(),
|
|
53
|
+
}];
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
publishers = (raw.publishers || []);
|
|
57
|
+
}
|
|
58
|
+
const migrated = {
|
|
59
|
+
customer_token: raw.customer_token || raw.token || null,
|
|
60
|
+
customer_email: raw.customer_email || raw.email || null,
|
|
61
|
+
publishers,
|
|
62
|
+
api_url: raw.api_url || 'https://api.getskillvault.com',
|
|
63
|
+
created_at: raw.setup_at || new Date().toISOString(),
|
|
64
|
+
};
|
|
65
|
+
// Persist in the new format so the next call hits the fast path.
|
|
66
|
+
try {
|
|
67
|
+
saveCredentials(migrated);
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
return migrated;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Persist customer credentials to ~/.skillvault/credentials.json with mode 0600.
|
|
78
|
+
*/
|
|
79
|
+
export function saveCredentials(creds) {
|
|
80
|
+
const path = credentialsPath();
|
|
81
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
82
|
+
writeFileSync(path, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
83
|
+
}
|
|
84
|
+
function projectConfigPath(roots) {
|
|
85
|
+
return join(roots.configDir, 'project.json');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Load this install's project config from <roots.configDir>/project.json.
|
|
89
|
+
* Returns null if no config exists yet.
|
|
90
|
+
*/
|
|
91
|
+
export function loadProjectConfig(roots) {
|
|
92
|
+
const path = projectConfigPath(roots);
|
|
93
|
+
try {
|
|
94
|
+
if (!existsSync(path))
|
|
95
|
+
return null;
|
|
96
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Persist project config to <roots.configDir>/project.json with mode 0600.
|
|
104
|
+
*/
|
|
105
|
+
export function saveProjectConfig(roots, config) {
|
|
106
|
+
mkdirSync(roots.configDir, { recursive: true });
|
|
107
|
+
writeFileSync(projectConfigPath(roots), JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
108
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects registry — tracks every known project install path.
|
|
3
|
+
*
|
|
4
|
+
* Lives at ~/.skillvault/projects.json (always global, regardless of install
|
|
5
|
+
* scope). Used by `--all` operations like `uninstall --all` and by
|
|
6
|
+
* `getCurrentProject()` to figure out whether the current CWD already has a
|
|
7
|
+
* registered SkillVault install.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, statSync } from 'node:fs';
|
|
10
|
+
import { dirname, resolve } from 'node:path';
|
|
11
|
+
import { projectsRegistryPath } from './scope.js';
|
|
12
|
+
const REGISTRY_VERSION = 1;
|
|
13
|
+
function readRegistry() {
|
|
14
|
+
const path = projectsRegistryPath();
|
|
15
|
+
if (!existsSync(path))
|
|
16
|
+
return { version: REGISTRY_VERSION, projects: [] };
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
19
|
+
if (Array.isArray(raw)) {
|
|
20
|
+
// Pre-versioned format: a bare array of entries.
|
|
21
|
+
return { version: REGISTRY_VERSION, projects: raw };
|
|
22
|
+
}
|
|
23
|
+
if (raw && typeof raw === 'object' && Array.isArray(raw.projects)) {
|
|
24
|
+
return raw;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
return { version: REGISTRY_VERSION, projects: [] };
|
|
29
|
+
}
|
|
30
|
+
function writeRegistry(reg) {
|
|
31
|
+
const path = projectsRegistryPath();
|
|
32
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
33
|
+
writeFileSync(path, JSON.stringify(reg, null, 2), { mode: 0o600 });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Return all registered project installs. Stale entries (paths that no longer
|
|
37
|
+
* exist on disk OR exist but are missing the .skillvault directory) are
|
|
38
|
+
* filtered out and the registry is rewritten on the way out so the next call
|
|
39
|
+
* is faster.
|
|
40
|
+
*
|
|
41
|
+
* Returns an empty array if the registry file does not exist or is malformed.
|
|
42
|
+
*/
|
|
43
|
+
export function listProjects() {
|
|
44
|
+
const reg = readRegistry();
|
|
45
|
+
const live = [];
|
|
46
|
+
let pruned = false;
|
|
47
|
+
for (const entry of reg.projects) {
|
|
48
|
+
const path = resolve(entry.path);
|
|
49
|
+
let stillThere = false;
|
|
50
|
+
try {
|
|
51
|
+
const st = statSync(path);
|
|
52
|
+
if (st.isDirectory()) {
|
|
53
|
+
// The .skillvault subdir is the install marker. If a customer rm -rf'd
|
|
54
|
+
// their project but kept the parent path, we shouldn't keep this in
|
|
55
|
+
// the registry either.
|
|
56
|
+
stillThere = existsSync(`${path}/.skillvault`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
if (stillThere) {
|
|
61
|
+
live.push(entry);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
pruned = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (pruned) {
|
|
68
|
+
try {
|
|
69
|
+
writeRegistry({ ...reg, projects: live });
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
}
|
|
73
|
+
return live;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Insert or update a project entry. Matching is by absolute path; if an entry
|
|
77
|
+
* with the same path already exists, it is replaced.
|
|
78
|
+
*/
|
|
79
|
+
export function registerProject(entry) {
|
|
80
|
+
const reg = readRegistry();
|
|
81
|
+
const normalized = { ...entry, path: resolve(entry.path) };
|
|
82
|
+
const idx = reg.projects.findIndex((p) => resolve(p.path) === normalized.path);
|
|
83
|
+
if (idx >= 0) {
|
|
84
|
+
reg.projects[idx] = normalized;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
reg.projects.push(normalized);
|
|
88
|
+
}
|
|
89
|
+
writeRegistry(reg);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Remove the project entry whose path matches. No-op if not present.
|
|
93
|
+
*/
|
|
94
|
+
export function unregisterProject(path) {
|
|
95
|
+
const reg = readRegistry();
|
|
96
|
+
const target = resolve(path);
|
|
97
|
+
const before = reg.projects.length;
|
|
98
|
+
reg.projects = reg.projects.filter((p) => resolve(p.path) !== target);
|
|
99
|
+
if (reg.projects.length !== before)
|
|
100
|
+
writeRegistry(reg);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Look up the registry entry that matches the current working directory (or
|
|
104
|
+
* the provided cwd). Returns null if there is no install registered for that
|
|
105
|
+
* directory.
|
|
106
|
+
*/
|
|
107
|
+
export function getCurrentProject(cwd) {
|
|
108
|
+
const target = resolve(cwd ?? process.cwd());
|
|
109
|
+
const reg = readRegistry();
|
|
110
|
+
return reg.projects.find((p) => resolve(p.path) === target) ?? null;
|
|
111
|
+
}
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive TTY prompts for the customer-facing CLI.
|
|
3
|
+
*
|
|
4
|
+
* Two prompts live here:
|
|
5
|
+
*
|
|
6
|
+
* confirmInstall(scope, roots) → Y/n/g — show install plan, get confirmation
|
|
7
|
+
* selectPublishers(available) → string[] — checkbox list for multi-publisher
|
|
8
|
+
*
|
|
9
|
+
* Both use raw stdin + ANSI escape codes (no extra dependencies). When stdin
|
|
10
|
+
* is not a TTY (CI, scripts, audit harness), they short-circuit:
|
|
11
|
+
* - confirmInstall returns 'yes'
|
|
12
|
+
* - selectPublishers returns every available publisher (caller can override
|
|
13
|
+
* with --publisher / --all flags before calling)
|
|
14
|
+
*/
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
/**
|
|
18
|
+
* Render the install plan for the resolved scope and ask the customer to
|
|
19
|
+
* confirm. Returns:
|
|
20
|
+
* 'yes' — Y or Enter (default)
|
|
21
|
+
* 'no' — n, cancels without writing anything
|
|
22
|
+
* 'global' — g, switches to global scope
|
|
23
|
+
*
|
|
24
|
+
* In non-TTY mode (CI, scripts, audit harness), returns 'yes' immediately
|
|
25
|
+
* without rendering the prompt.
|
|
26
|
+
*/
|
|
27
|
+
export async function confirmInstall(scope, roots, cwd = process.cwd()) {
|
|
28
|
+
const lines = renderInstallPlan(scope, roots, cwd);
|
|
29
|
+
for (const line of lines)
|
|
30
|
+
console.error(line);
|
|
31
|
+
if (!process.stdin.isTTY) {
|
|
32
|
+
console.error(' (non-TTY: auto-confirming)');
|
|
33
|
+
return 'yes';
|
|
34
|
+
}
|
|
35
|
+
process.stderr.write('Continue? [Y/n/g] (Y=yes, n=cancel, g=install globally instead): ');
|
|
36
|
+
const key = await readSingleKey();
|
|
37
|
+
console.error('');
|
|
38
|
+
if (key === 'n' || key === 'N')
|
|
39
|
+
return 'no';
|
|
40
|
+
if (key === 'g' || key === 'G')
|
|
41
|
+
return 'global';
|
|
42
|
+
// Y, Enter, or anything else → yes (the safe default)
|
|
43
|
+
return 'yes';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build the human-readable preview lines for the install plan. Returned as
|
|
47
|
+
* an array so callers can also render via dry-run output if they want.
|
|
48
|
+
*/
|
|
49
|
+
export function renderInstallPlan(scope, roots, cwd) {
|
|
50
|
+
const lines = [];
|
|
51
|
+
lines.push('');
|
|
52
|
+
lines.push('🔐 SkillVault Setup');
|
|
53
|
+
lines.push('');
|
|
54
|
+
lines.push(`Installing for: ${scope === 'project' ? 'PROJECT (this directory)' : 'GLOBAL (~/.claude, ~/.skillvault)'}`);
|
|
55
|
+
lines.push(`Location: ${scope === 'project' ? cwd : homedir()}`);
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push('Will write:');
|
|
58
|
+
if (scope === 'project') {
|
|
59
|
+
lines.push(` ./.claude/settings.json (Claude Code hooks)`);
|
|
60
|
+
lines.push(` ./.claude/skills/<skill>/ (skill stubs)`);
|
|
61
|
+
lines.push(` ./.skillvault/project.json (project config)`);
|
|
62
|
+
lines.push(` ./.skillvault/vaults/ (encrypted skill cache)`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
lines.push(` ~/.claude/settings.json (Claude Code hooks)`);
|
|
66
|
+
lines.push(` ~/.claude/skills/<skill>/ (skill stubs)`);
|
|
67
|
+
lines.push(` ~/.skillvault/project.json (install metadata)`);
|
|
68
|
+
lines.push(` ~/.skillvault/vaults/ (encrypted skill cache)`);
|
|
69
|
+
lines.push(` ~/.agents/skills/<skill>/ (agent-agnostic mirror)`);
|
|
70
|
+
}
|
|
71
|
+
lines.push('');
|
|
72
|
+
lines.push('Plus user-level (always global):');
|
|
73
|
+
lines.push(` ~/.skillvault/credentials.json (your customer identity)`);
|
|
74
|
+
lines.push(` ~/.skillvault/projects.json (project install registry)`);
|
|
75
|
+
lines.push('');
|
|
76
|
+
// Helps callers debug — paths from the resolved roots, in case the spec
|
|
77
|
+
// ever drifts from what we render above.
|
|
78
|
+
void join(roots.configDir, 'project.json');
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Render a checkbox list of publishers and let the customer toggle which
|
|
83
|
+
* to enable in this install. Returns the selected publisher IDs.
|
|
84
|
+
*
|
|
85
|
+
* Controls:
|
|
86
|
+
* ↑ / ↓ navigate
|
|
87
|
+
* space toggle the focused entry
|
|
88
|
+
* a toggle all
|
|
89
|
+
* enter confirm and exit
|
|
90
|
+
* q / esc cancel (returns the original `preselected` set)
|
|
91
|
+
*
|
|
92
|
+
* In non-TTY mode this throws — callers should catch and use --publisher /
|
|
93
|
+
* --all flags or fall back to a sensible default.
|
|
94
|
+
*/
|
|
95
|
+
export async function selectPublishers(available, preselected = []) {
|
|
96
|
+
if (!process.stdin.isTTY) {
|
|
97
|
+
throw new Error('selectPublishers: not a TTY (use --publisher or --all flags)');
|
|
98
|
+
}
|
|
99
|
+
if (available.length === 0)
|
|
100
|
+
return [];
|
|
101
|
+
const selected = new Set(preselected);
|
|
102
|
+
let cursor = 0;
|
|
103
|
+
const draw = () => {
|
|
104
|
+
process.stderr.write('\x1b[?25l'); // hide cursor
|
|
105
|
+
process.stderr.write(`\nYou have access to ${available.length} publishers. Which to enable in this project?\n\n`);
|
|
106
|
+
for (let i = 0; i < available.length; i++) {
|
|
107
|
+
const p = available[i];
|
|
108
|
+
const focused = i === cursor;
|
|
109
|
+
const checked = selected.has(p.id);
|
|
110
|
+
const box = checked ? '[x]' : '[ ]';
|
|
111
|
+
const arrow = focused ? '>' : ' ';
|
|
112
|
+
const skillSuffix = p.skillCount !== undefined ? ` — ${p.skillCount} skill${p.skillCount !== 1 ? 's' : ''}` : '';
|
|
113
|
+
process.stderr.write(` ${arrow} ${box} ${p.name.padEnd(20)} ${skillSuffix}\n`);
|
|
114
|
+
}
|
|
115
|
+
process.stderr.write('\nSpace=toggle a=toggle all Enter=confirm q=cancel\n');
|
|
116
|
+
};
|
|
117
|
+
const erase = () => {
|
|
118
|
+
// Clear the rendered lines so the next draw replaces them in place.
|
|
119
|
+
const lines = available.length + 4;
|
|
120
|
+
for (let i = 0; i < lines; i++) {
|
|
121
|
+
process.stderr.write('\x1b[1A\x1b[2K');
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
draw();
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
const stdin = process.stdin;
|
|
127
|
+
stdin.setRawMode(true);
|
|
128
|
+
stdin.resume();
|
|
129
|
+
stdin.setEncoding('utf8');
|
|
130
|
+
const cleanup = () => {
|
|
131
|
+
stdin.setRawMode(false);
|
|
132
|
+
stdin.pause();
|
|
133
|
+
stdin.removeListener('data', onData);
|
|
134
|
+
process.stderr.write('\x1b[?25h'); // show cursor
|
|
135
|
+
};
|
|
136
|
+
const onData = (key) => {
|
|
137
|
+
if (key === '\u0003' || key === 'q' || key === '\u001b') {
|
|
138
|
+
// Ctrl-C, q, or Esc — cancel
|
|
139
|
+
erase();
|
|
140
|
+
cleanup();
|
|
141
|
+
resolve([...preselected]);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (key === '\r' || key === '\n') {
|
|
145
|
+
erase();
|
|
146
|
+
cleanup();
|
|
147
|
+
resolve([...selected]);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (key === ' ') {
|
|
151
|
+
const id = available[cursor]?.id;
|
|
152
|
+
if (id) {
|
|
153
|
+
if (selected.has(id))
|
|
154
|
+
selected.delete(id);
|
|
155
|
+
else
|
|
156
|
+
selected.add(id);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (key === 'a' || key === 'A') {
|
|
160
|
+
if (selected.size === available.length)
|
|
161
|
+
selected.clear();
|
|
162
|
+
else
|
|
163
|
+
for (const p of available)
|
|
164
|
+
selected.add(p.id);
|
|
165
|
+
}
|
|
166
|
+
else if (key === '\u001b[A' || key === 'k') {
|
|
167
|
+
cursor = (cursor - 1 + available.length) % available.length;
|
|
168
|
+
}
|
|
169
|
+
else if (key === '\u001b[B' || key === 'j') {
|
|
170
|
+
cursor = (cursor + 1) % available.length;
|
|
171
|
+
}
|
|
172
|
+
erase();
|
|
173
|
+
draw();
|
|
174
|
+
};
|
|
175
|
+
stdin.on('data', onData);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** Read a single keystroke from stdin. Used by confirmInstall. */
|
|
179
|
+
function readSingleKey() {
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
const stdin = process.stdin;
|
|
182
|
+
stdin.setRawMode(true);
|
|
183
|
+
stdin.resume();
|
|
184
|
+
stdin.setEncoding('utf8');
|
|
185
|
+
const onData = (key) => {
|
|
186
|
+
stdin.setRawMode(false);
|
|
187
|
+
stdin.pause();
|
|
188
|
+
stdin.removeListener('data', onData);
|
|
189
|
+
resolve(key);
|
|
190
|
+
};
|
|
191
|
+
stdin.on('data', onData);
|
|
192
|
+
});
|
|
193
|
+
}
|
package/dist/scope.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install scope resolution.
|
|
3
|
+
*
|
|
4
|
+
* SkillVault supports two install scopes:
|
|
5
|
+
* - "project": files live in the current working directory (./.skillvault, ./.claude)
|
|
6
|
+
* - "global": files live under the user's home directory (~/.skillvault, ~/.claude)
|
|
7
|
+
*
|
|
8
|
+
* Customer credentials (~/.skillvault/credentials.json) are ALWAYS global —
|
|
9
|
+
* they represent the user's identity, not the install. The scope only affects
|
|
10
|
+
* where vault data, project config, hooks, and skill stubs are written.
|
|
11
|
+
*/
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { loadCredentials, loadProjectConfig } from './credentials.js';
|
|
15
|
+
import { getCurrentProject } from './projects-registry.js';
|
|
16
|
+
/**
|
|
17
|
+
* Determine install scope from explicit flags and the current working directory.
|
|
18
|
+
*
|
|
19
|
+
* Priority:
|
|
20
|
+
* 1. --global flag → global
|
|
21
|
+
* 2. --project flag → project
|
|
22
|
+
* 3. CWD === $HOME or '/' → global (no sane "project" location)
|
|
23
|
+
* 4. Otherwise → project
|
|
24
|
+
*/
|
|
25
|
+
export function resolveScope(opts = {}) {
|
|
26
|
+
if (opts.global)
|
|
27
|
+
return 'global';
|
|
28
|
+
if (opts.project)
|
|
29
|
+
return 'project';
|
|
30
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
31
|
+
const home = homedir();
|
|
32
|
+
if (cwd === home || cwd === '/')
|
|
33
|
+
return 'global';
|
|
34
|
+
return 'project';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve filesystem roots for a given scope.
|
|
38
|
+
*
|
|
39
|
+
* For project scope, roots are based on `cwd` (defaults to process.cwd()).
|
|
40
|
+
* For global scope, roots are based on the user's home directory.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveRoots(scope, cwd) {
|
|
43
|
+
if (scope === 'project') {
|
|
44
|
+
const base = cwd ?? process.cwd();
|
|
45
|
+
const configDir = join(base, '.skillvault');
|
|
46
|
+
const claudeDir = join(base, '.claude');
|
|
47
|
+
return {
|
|
48
|
+
scope,
|
|
49
|
+
configDir,
|
|
50
|
+
claudeDir,
|
|
51
|
+
vaultDir: join(configDir, 'vaults'),
|
|
52
|
+
settingsPath: join(claudeDir, 'settings.json'),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const home = homedir();
|
|
56
|
+
const configDir = join(home, '.skillvault');
|
|
57
|
+
const claudeDir = join(home, '.claude');
|
|
58
|
+
return {
|
|
59
|
+
scope,
|
|
60
|
+
configDir,
|
|
61
|
+
claudeDir,
|
|
62
|
+
vaultDir: join(configDir, 'vaults'),
|
|
63
|
+
settingsPath: join(claudeDir, 'settings.json'),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Always-global path to the customer credentials file.
|
|
68
|
+
* Credentials represent the user's identity and are never project-scoped.
|
|
69
|
+
*/
|
|
70
|
+
export function credentialsPath() {
|
|
71
|
+
return join(homedir(), '.skillvault', 'credentials.json');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Always-global path to the projects registry.
|
|
75
|
+
* Tracks every known project install path for `--all` operations.
|
|
76
|
+
*/
|
|
77
|
+
export function projectsRegistryPath() {
|
|
78
|
+
return join(homedir(), '.skillvault', 'projects.json');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Report the current install state at CWD.
|
|
82
|
+
*
|
|
83
|
+
* Resolves scope using the same rules as resolveScope() (no flags), looks
|
|
84
|
+
* up matching credentials, project config, and registry entry, and returns
|
|
85
|
+
* a single object suitable for the `--status` command and confirmation
|
|
86
|
+
* prompts. Fields that don't exist are returned as null/empty rather than
|
|
87
|
+
* throwing.
|
|
88
|
+
*/
|
|
89
|
+
export function reportInstallStatus(opts = {}) {
|
|
90
|
+
const scope = resolveScope(opts);
|
|
91
|
+
const roots = resolveRoots(scope, opts.cwd);
|
|
92
|
+
const credentials = loadCredentials();
|
|
93
|
+
const projectConfig = loadProjectConfig(roots);
|
|
94
|
+
const projectEntry = scope === 'project' ? getCurrentProject(opts.cwd) : null;
|
|
95
|
+
const activePublishers = projectConfig?.active_publishers
|
|
96
|
+
?? projectEntry?.active_publishers
|
|
97
|
+
?? [];
|
|
98
|
+
return { scope, roots, projectEntry, credentials, projectConfig, activePublishers };
|
|
99
|
+
}
|