@jacebenson/jsn 3.3.0 → 3.3.1
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/package.json +1 -1
- package/scripts/postinstall.mjs +4 -0
- package/src/cli.js +23 -2
- package/src/commands/skill.js +74 -76
package/package.json
CHANGED
package/scripts/postinstall.mjs
CHANGED
|
@@ -55,6 +55,10 @@ function installSkill() {
|
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// Symlinks require admin on Windows and are pointless outside Unix/Mac.
|
|
59
|
+
// Skip on win32 to avoid "system cannot find the path" errors.
|
|
60
|
+
if (process.platform === 'win32') return;
|
|
61
|
+
|
|
58
62
|
// Create symlink at ~/.claude/skills/servicenow → ../../.agents/skills/servicenow
|
|
59
63
|
// This is the same pattern basecamp's installer uses: ~/.claude/skills/basecamp → ../../.agents/skills/basecamp
|
|
60
64
|
try {
|
package/src/cli.js
CHANGED
|
@@ -210,10 +210,31 @@ export function buildCLI() {
|
|
|
210
210
|
|| argv.json
|
|
211
211
|
|| argv.quiet;
|
|
212
212
|
|
|
213
|
-
// Auto-check skill on every command (
|
|
213
|
+
// Auto-check skill on every command (once per 24h, non-blocking)
|
|
214
214
|
const skipSkillCheck = ['help', 'version', 'completion', 'skill', 'docs'].includes(cmd)
|
|
215
215
|
|| process.env.JSN_NO_SKILL_CHECK === '1'
|
|
216
|
-
|| argv['no-skill-check']
|
|
216
|
+
|| argv['no-skill-check'];
|
|
217
|
+
if (!skipSkillCheck) {
|
|
218
|
+
const skillCacheFile = path.join(globalConfigDir(), '.last-skill-check');
|
|
219
|
+
let shouldCheckSkill = true;
|
|
220
|
+
try {
|
|
221
|
+
const mtime = fs.statSync(skillCacheFile).mtimeMs;
|
|
222
|
+
if (Date.now() - mtime < 86400000) shouldCheckSkill = false;
|
|
223
|
+
} catch {
|
|
224
|
+
// File doesn't exist — check
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (shouldCheckSkill) {
|
|
228
|
+
checkSkill().catch(() => {});
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
fs.mkdirSync(globalConfigDir(), { recursive: true });
|
|
232
|
+
fs.writeFileSync(skillCacheFile, String(Date.now()), 'utf-8');
|
|
233
|
+
} catch {
|
|
234
|
+
// Non-fatal
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
217
238
|
if (!skipNpmCheck) {
|
|
218
239
|
const cacheFile = path.join(globalConfigDir(), '.last-npm-check');
|
|
219
240
|
let shouldCheck = true;
|
package/src/commands/skill.js
CHANGED
|
@@ -6,6 +6,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
|
|
7
7
|
import { execSync } from 'node:child_process';
|
|
8
8
|
|
|
9
|
+
import { globalConfigPath } from '../config.js';
|
|
10
|
+
|
|
9
11
|
const SKILL_NAME = 'servicenow';
|
|
10
12
|
const SKILL_REPO_PATH = path.join(
|
|
11
13
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -94,73 +96,75 @@ function readBundledSkill() {
|
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const installedCopies = [];
|
|
106
|
-
for (const [key, dir] of Object.entries(AGENT_SKILL_DIRS)) {
|
|
107
|
-
const skillPath = key === 'cursor'
|
|
108
|
-
? path.join(dir, `${SKILL_NAME}.mdc`)
|
|
109
|
-
: path.join(dir, 'SKILL.md');
|
|
110
|
-
try {
|
|
111
|
-
const content = fs.readFileSync(skillPath, 'utf-8');
|
|
112
|
-
installedCopies.push({ harness: key, label: TARGET_NAMES[key], content, path: skillPath });
|
|
113
|
-
} catch {
|
|
114
|
-
// Not installed to this harness — skip
|
|
115
|
-
}
|
|
99
|
+
// ── Skill config helpers — read/write skillLocation / skillVersion / skillLastChecked ──
|
|
100
|
+
|
|
101
|
+
function loadSkillConfig() {
|
|
102
|
+
try {
|
|
103
|
+
const raw = fs.readFileSync(globalConfigPath(), 'utf-8');
|
|
104
|
+
return JSON.parse(raw);
|
|
105
|
+
} catch {
|
|
106
|
+
return {};
|
|
116
107
|
}
|
|
108
|
+
}
|
|
117
109
|
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
110
|
+
function saveSkillConfig(updates) {
|
|
111
|
+
const cfg = loadSkillConfig();
|
|
112
|
+
Object.assign(cfg, updates);
|
|
113
|
+
fs.mkdirSync(path.dirname(globalConfigPath()), { recursive: true });
|
|
114
|
+
fs.writeFileSync(globalConfigPath(), JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
115
|
+
}
|
|
121
116
|
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
function recordSkillInstall(targetPath) {
|
|
118
|
+
const bundled = readBundledSkill();
|
|
119
|
+
const version = extractVersion(bundled) || 'unknown';
|
|
120
|
+
saveSkillConfig({
|
|
121
|
+
skillLocation: targetPath,
|
|
122
|
+
skillVersion: version,
|
|
123
|
+
skillLastChecked: new Date().toISOString(),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
124
126
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const upstream = await res.text();
|
|
132
|
-
const upstreamVersion = extractVersion(upstream);
|
|
133
|
-
if (!upstreamVersion) return { current: false, error: 'No version field in upstream skill' };
|
|
134
|
-
|
|
135
|
-
const current = installedVersion === upstreamVersion;
|
|
136
|
-
|
|
137
|
-
// Check each installed copy for version match
|
|
138
|
-
const outdated = [];
|
|
139
|
-
for (const copy of installedCopies) {
|
|
140
|
-
const v = extractVersion(copy.content);
|
|
141
|
-
if (v && v !== upstreamVersion) {
|
|
142
|
-
outdated.push(copy.label);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
127
|
+
// ── Skill version check (once per day, uses recorded location) ──
|
|
128
|
+
|
|
129
|
+
export function checkSkill() {
|
|
130
|
+
const cfg = loadSkillConfig();
|
|
131
|
+
const location = cfg.skillLocation;
|
|
132
|
+
if (!location) return { current: true, note: 'Skill not installed — run "jsn skill install"' };
|
|
145
133
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
harnesses: {
|
|
156
|
-
installed: installedCopies.map(c => c.label),
|
|
157
|
-
outdated,
|
|
158
|
-
},
|
|
159
|
-
error: current ? null : outdatedMsg,
|
|
160
|
-
};
|
|
134
|
+
// Throttle: skip if checked within last 24 hours
|
|
135
|
+
if (cfg.skillLastChecked) {
|
|
136
|
+
const elapsed = Date.now() - new Date(cfg.skillLastChecked).getTime();
|
|
137
|
+
if (elapsed < 86400000) return { current: true, note: 'Checked within 24h — skipping' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let installed;
|
|
141
|
+
try {
|
|
142
|
+
installed = fs.readFileSync(location, 'utf-8');
|
|
161
143
|
} catch {
|
|
162
|
-
return { current:
|
|
144
|
+
return { current: true, note: `Skill file not found at ${location} — run "jsn skill install"` };
|
|
163
145
|
}
|
|
146
|
+
|
|
147
|
+
const installedVersion = extractVersion(installed);
|
|
148
|
+
const bundled = readBundledSkill();
|
|
149
|
+
const bundledVersion = extractVersion(bundled);
|
|
150
|
+
|
|
151
|
+
if (!installedVersion || !bundledVersion) {
|
|
152
|
+
return { current: true, error: 'Could not determine skill version' };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const current = installedVersion === bundledVersion;
|
|
156
|
+
|
|
157
|
+
if (!current) {
|
|
158
|
+
process.stderr.write(
|
|
159
|
+
`\n⚠ jsn: ServiceNow skill is outdated (installed v${installedVersion}, bundled v${bundledVersion}).\n` +
|
|
160
|
+
` Run "jsn skill install" to update.\n\n`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Update last-checked timestamp
|
|
165
|
+
saveSkillConfig({ skillLastChecked: new Date().toISOString() });
|
|
166
|
+
|
|
167
|
+
return { current, installed_version: installedVersion, bundled_version: bundledVersion };
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
function extractVersion(content) {
|
|
@@ -196,28 +200,19 @@ export function skillCmd(wrap) {
|
|
|
196
200
|
})
|
|
197
201
|
.command({
|
|
198
202
|
command: 'check',
|
|
199
|
-
describe: 'Check if the
|
|
203
|
+
describe: 'Check if the installed skill matches the bundled version',
|
|
200
204
|
handler: wrap(async (_argv, app) => {
|
|
201
|
-
const result =
|
|
202
|
-
const h = result.harnesses || {};
|
|
205
|
+
const result = checkSkill();
|
|
203
206
|
|
|
204
|
-
// Build a summary that shows which harnesses are installed and current
|
|
205
207
|
let summary;
|
|
206
|
-
if (result.error
|
|
208
|
+
if (result.error) {
|
|
207
209
|
summary = result.error;
|
|
210
|
+
} else if (result.note) {
|
|
211
|
+
summary = result.note;
|
|
208
212
|
} else if (result.current) {
|
|
209
|
-
|
|
210
|
-
if (h.installed && h.installed.length > 0) {
|
|
211
|
-
parts.push(` — installed for ${h.installed.join(', ')}`);
|
|
212
|
-
}
|
|
213
|
-
summary = parts.join('');
|
|
213
|
+
summary = `✓ Skill is current (v${result.installed_version} matches bundled v${result.bundled_version})`;
|
|
214
214
|
} else {
|
|
215
|
-
|
|
216
|
-
if (h.outdated && h.outdated.length > 0) {
|
|
217
|
-
parts.push(` — outdated for ${h.outdated.join(', ')}`);
|
|
218
|
-
}
|
|
219
|
-
parts.push(` — run "jsn skill install" to update`);
|
|
220
|
-
summary = parts.join('');
|
|
215
|
+
summary = `⚠ Skill outdated: installed v${result.installed_version} vs bundled v${result.bundled_version} — run "jsn skill install" to update`;
|
|
221
216
|
}
|
|
222
217
|
|
|
223
218
|
app.ok(result, { summary });
|
|
@@ -386,6 +381,9 @@ export function skillCmd(wrap) {
|
|
|
386
381
|
throw new Error(`No targets matched. Valid targets: ${Object.keys(AGENT_SKILL_DIRS).join(', ')}, or "all"`);
|
|
387
382
|
}
|
|
388
383
|
|
|
384
|
+
// Record install location so checkSkill knows where to look
|
|
385
|
+
recordSkillInstall(targets[0].path);
|
|
386
|
+
|
|
389
387
|
const installed = targets.reduce((acc, t) => { acc[t.name] = t.path; return acc; }, {});
|
|
390
388
|
const summary = targets.length === 1
|
|
391
389
|
? `Skill installed to ${targets[0].path}`
|