wyvrnpm 2.4.1 → 2.9.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.
@@ -3,8 +3,35 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { defaultManifest, writeManifest } = require('../manifest');
6
+ const { LOCAL_OVERLAY_FILENAME } = require('../conf');
6
7
  const log = require('../logger');
7
8
 
9
+ // The wyvrn.local.json overlay must never be committed — it is by design
10
+ // per-developer state. init writes a gitignore entry; existing projects
11
+ // get a one-line nudge from install (see install.js) when the file is
12
+ // present but not ignored.
13
+ const GITIGNORE_MARKER_LINE = '# wyvrnpm — per-developer conf overlay (never commit)';
14
+
15
+ function ensureGitignoreEntry(rootDir) {
16
+ const gitignorePath = path.join(rootDir, '.gitignore');
17
+ const desiredLines = [GITIGNORE_MARKER_LINE, LOCAL_OVERLAY_FILENAME];
18
+
19
+ let existing = '';
20
+ if (fs.existsSync(gitignorePath)) {
21
+ existing = fs.readFileSync(gitignorePath, 'utf8');
22
+ const hasEntry = existing.split(/\r?\n/).some((l) => l.trim() === LOCAL_OVERLAY_FILENAME);
23
+ if (hasEntry) return { action: 'already-present', path: gitignorePath };
24
+ }
25
+
26
+ const needsLeadingNewline = existing.length > 0 && !existing.endsWith('\n');
27
+ const block = (needsLeadingNewline ? '\n' : '') + desiredLines.join('\n') + '\n';
28
+ fs.writeFileSync(gitignorePath, existing + block, 'utf8');
29
+ return {
30
+ action: existing.length > 0 ? 'appended' : 'created',
31
+ path: gitignorePath,
32
+ };
33
+ }
34
+
8
35
  /**
9
36
  * Initialises a new wyvrn.json manifest in the project root.
10
37
  * If a manifest already exists the command exits early without overwriting it.
@@ -28,7 +55,16 @@ async function init(argv) {
28
55
 
29
56
  writeManifest(manifestPath, manifest);
30
57
 
58
+ const gitignore = ensureGitignoreEntry(rootDir);
59
+ if (gitignore.action === 'created') {
60
+ log.info(`Created .gitignore at ${gitignore.path} with ${LOCAL_OVERLAY_FILENAME} exclusion`);
61
+ } else if (gitignore.action === 'appended') {
62
+ log.info(`Added ${LOCAL_OVERLAY_FILENAME} to ${gitignore.path}`);
63
+ }
64
+
31
65
  log.success(`Created manifest at ${manifestPath}`);
32
66
  }
33
67
 
34
68
  module.exports = init;
69
+ module.exports.ensureGitignoreEntry = ensureGitignoreEntry;
70
+ module.exports.GITIGNORE_MARKER_LINE = GITIGNORE_MARKER_LINE;
@@ -5,6 +5,7 @@ const os = require('os');
5
5
  const path = require('path');
6
6
  const AdmZip = require('adm-zip');
7
7
 
8
+ const { assertAllSafeZipEntryNames } = require('../zip-safe');
8
9
  const log = require('../logger');
9
10
 
10
11
  // ---------------------------------------------------------------------------
@@ -72,6 +73,13 @@ function installForClaude({ force, dryRun }) {
72
73
  rmrf(destZip);
73
74
 
74
75
  const zip = new AdmZip(zipPath);
76
+ // Zip Slip defense-in-depth (EVALUATION.md S2). The shipped .skill zip
77
+ // is trusted-by-provenance, but validating consistently keeps the
78
+ // attack surface flat across all extraction call sites.
79
+ assertAllSafeZipEntryNames(
80
+ zip.getEntries().map((e) => e.entryName),
81
+ `${SKILL_NAME}.skill`,
82
+ );
75
83
  zip.extractAllTo(skillsDir, /* overwrite */ true);
76
84
  fs.copyFileSync(zipPath, destZip);
77
85