srclawyer 0.4.0 → 0.5.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/README.md CHANGED
@@ -8,16 +8,58 @@ Static analysis engine that derives privacy policy documentation directly from s
8
8
 
9
9
  ## Usage
10
10
 
11
+ No install needed for a first try — `npx` fetches and runs the latest version on demand:
12
+
13
+ ```bash
14
+ npx srclawyer init # one-time: jurisdiction, markets, industry
15
+ npx srclawyer scan # analyze the codebase and write PRIVACY_POLICY.md
16
+ ```
17
+
18
+ For a persistent `srclawyer` command instead of typing `npx` every time:
19
+
11
20
  ```bash
21
+ npm install -g srclawyer
22
+ srclawyer init
23
+ srclawyer scan # analyze the codebase and write PRIVACY_POLICY.md
24
+ srclawyer scan --json # findings as JSON (still writes the policy file)
25
+ srclawyer scan --out docs/privacy.md
26
+ ```
27
+
28
+ If `srclawyer` isn't found right after a global install, see [Troubleshooting: command not found](#troubleshooting-command-not-found) below.
29
+
30
+ ## Development
31
+
32
+ Working on SrcLawyer itself (not just using it):
33
+
34
+ ```bash
35
+ git clone https://github.com/SrcLawyer/srclawyer.git
36
+ cd srclawyer
12
37
  npm install
13
38
  npm run build
14
- node dist/cli.js init # one-time: jurisdiction, markets, industry
15
- node dist/cli.js scan # analyze the codebase and write PRIVACY_POLICY.md
16
- node dist/cli.js scan --json # findings as JSON (still writes the policy file)
17
- node dist/cli.js scan --out docs/privacy.md
39
+ node dist/cli.js scan
18
40
  ```
19
41
 
20
- During development, run directly from source with `npm run dev -- scan`.
42
+ Or run directly from source without building first: `npm run dev -- scan`.
43
+
44
+ ## Troubleshooting: command not found
45
+
46
+ After `npm install -g srclawyer`, the install prints `added N packages` but the `srclawyer` command isn't recognized. This almost always means the command *was* installed — npm's own global bin directory just isn't on your shell's `PATH`, or your shell doesn't know about it yet.
47
+
48
+ **The most common cause: conda.** If you have Anaconda/Miniconda installed with `auto_activate_base` enabled (the default), conda prepends its own directories to `PATH` every time a new shell starts — this can land ahead of, or instead of, the directory npm actually installed into. Symptoms: `srclawyer` works in some terminals/tabs but not others, or never works despite the install reporting success.
49
+
50
+ Steps, in order of how likely each is to fix it:
51
+
52
+ 1. **Open a brand-new terminal window or tab.** `PATH` is set when a shell starts; a terminal that was already open won't pick up a global install that happened after it launched.
53
+ 2. **If a new terminal doesn't help, check where npm actually put it versus what's on your `PATH`:**
54
+ ```bash
55
+ npm config get prefix # where npm installs global packages
56
+ echo $PATH # does the above, plus "/bin", appear in here?
57
+ ```
58
+ If `npm config get prefix`'s `bin` subdirectory (or, on Windows, the prefix itself) isn't listed in `$PATH` at all, that's the root cause — your shell's startup files (`.zshrc`, `.bashrc`, `.bash_profile`) need a line adding it, e.g. `export PATH="$(npm config get prefix)/bin:$PATH"`.
59
+ 3. **If conda specifically is the culprit** (its paths appear in `$PATH` ahead of npm's prefix, or npm's prefix is missing because conda's own Node/npm shadowed the one you expected to use): either run `conda deactivate` before using `srclawyer`, or turn off automatic activation so it stops happening on every new shell: `conda config --set auto_activate_base false`.
60
+ 4. **If `srclawyer` still isn't found after fixing `PATH`**, your shell may have cached the old (missing) command location. Clear that cache: `hash -r` (bash) or `rehash` (zsh), then try again without opening a new terminal.
61
+
62
+ If none of this resolves it, `npx srclawyer scan` (see [Usage](#usage) above) sidesteps `PATH` entirely, since `npx` always invokes the binary directly rather than relying on it being a recognized shell command.
21
63
 
22
64
  ## What `scan` does today
23
65
 
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
- import { CONFIG_FILENAME, DEFAULT_CONFIG, configPath, writeConfig } from "../config/config.js";
4
+ import { CONFIG_FILENAME, DEFAULT_CONFIG, configPath, hasUnmigratedLegacyConfig, legacyConfigMessage, writeConfig, } from "../config/config.js";
5
5
  function guessIndustry(root) {
6
6
  const pkgPath = join(root, "package.json");
7
7
  if (!existsSync(pkgPath))
@@ -29,6 +29,10 @@ export async function runInit(root) {
29
29
  console.log(`${CONFIG_FILENAME} already exists. Delete it first if you want to redo setup.`);
30
30
  return;
31
31
  }
32
+ if (hasUnmigratedLegacyConfig(root)) {
33
+ console.log(legacyConfigMessage());
34
+ return;
35
+ }
32
36
  const guessedIndustry = guessIndustry(root);
33
37
  const rl = createInterface({ input: process.stdin, output: process.stdout });
34
38
  const lines = rl[Symbol.asyncIterator]();
@@ -1,6 +1,6 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { loadConfig } from "../config/config.js";
3
+ import { CONFIG_FILENAME, hasUnmigratedLegacyConfig, legacyConfigMessage, loadConfig } from "../config/config.js";
4
4
  import { scan } from "../engine/scanEngine.js";
5
5
  import { formatText } from "../report/formatText.js";
6
6
  import { generatePolicyMarkdown } from "../policy/generatePolicy.js";
@@ -10,8 +10,15 @@ import { resolveProtectedLogic } from "../cloud/resolveProtectedLogic.js";
10
10
  import { resolveProtectedLogicEndpoint } from "../cloud/config.js";
11
11
  export async function runScan(root, options) {
12
12
  const config = loadConfig(root);
13
+ let legacyConfigWarning = null;
13
14
  if (!config) {
14
- console.error('No .privacypolicy.yml found. Run "srclawyer init" first.\n');
15
+ if (hasUnmigratedLegacyConfig(root)) {
16
+ legacyConfigWarning = legacyConfigMessage();
17
+ console.error(legacyConfigWarning);
18
+ }
19
+ else {
20
+ console.error(`No ${CONFIG_FILENAME} found. Run "srclawyer init" first.\n`);
21
+ }
15
22
  }
16
23
  const result = await scan(root);
17
24
  let findings = result.findings;
@@ -52,11 +59,11 @@ export async function runScan(root, options) {
52
59
  const finalResult = { ...result, findings, ambiguousCount };
53
60
  const policyPath = resolve(root, options.out);
54
61
  writeFileSync(policyPath, generatePolicyMarkdown(finalResult, config, llmError));
55
- if (result.unsupportedStackWarning || llmError || protectedLogicWarning) {
62
+ if (result.unsupportedStackWarning || llmError || protectedLogicWarning || legacyConfigWarning) {
56
63
  process.exitCode = 1;
57
64
  }
58
65
  if (options.json) {
59
- console.log(JSON.stringify({ ...finalResult, policyPath, llmError, protectedLogicWarning }, null, 2));
66
+ console.log(JSON.stringify({ ...finalResult, policyPath, llmError, protectedLogicWarning, legacyConfigWarning }, null, 2));
60
67
  return;
61
68
  }
62
69
  console.log(formatText(finalResult));
@@ -1,4 +1,6 @@
1
- export declare const CONFIG_FILENAME = ".privacypolicy.yml";
1
+ export declare const CONFIG_FILENAME = ".srclawyer.config.yml";
2
+ /** The pre-0.5.0 name. Never auto-migrated or silently read as a fallback -- see legacyConfigMessage. */
3
+ export declare const LEGACY_CONFIG_FILENAME = ".privacypolicy.yml";
2
4
  export type SrcLawyerTier = "free" | "byok" | "managed";
3
5
  export interface SrcLawyerLlmConfig {
4
6
  provider: "anthropic";
@@ -14,5 +16,15 @@ export interface SrcLawyerConfig {
14
16
  }
15
17
  export declare const DEFAULT_CONFIG: SrcLawyerConfig;
16
18
  export declare function configPath(root: string): string;
19
+ export declare function legacyConfigPath(root: string): string;
20
+ /** True only when the old file is present and the new one isn't -- never fires once migrated. */
21
+ export declare function hasUnmigratedLegacyConfig(root: string): boolean;
22
+ /**
23
+ * Single source of truth for this message so init and scan can't drift into saying it two different
24
+ * ways. Deliberately never auto-reads the legacy file as a fallback, even though the YAML shape is
25
+ * identical -- the rename needs to be an explicit, visible action, not a silent compatibility shim
26
+ * that lets the old filename linger indefinitely.
27
+ */
28
+ export declare function legacyConfigMessage(): string;
17
29
  export declare function loadConfig(root: string): SrcLawyerConfig | null;
18
30
  export declare function writeConfig(root: string, config: SrcLawyerConfig): void;
@@ -1,7 +1,9 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import yaml from "js-yaml";
4
- export const CONFIG_FILENAME = ".privacypolicy.yml";
4
+ export const CONFIG_FILENAME = ".srclawyer.config.yml";
5
+ /** The pre-0.5.0 name. Never auto-migrated or silently read as a fallback -- see legacyConfigMessage. */
6
+ export const LEGACY_CONFIG_FILENAME = ".privacypolicy.yml";
5
7
  export const DEFAULT_CONFIG = {
6
8
  entityLocation: null,
7
9
  targetMarkets: [],
@@ -12,6 +14,22 @@ export const DEFAULT_CONFIG = {
12
14
  export function configPath(root) {
13
15
  return join(root, CONFIG_FILENAME);
14
16
  }
17
+ export function legacyConfigPath(root) {
18
+ return join(root, LEGACY_CONFIG_FILENAME);
19
+ }
20
+ /** True only when the old file is present and the new one isn't -- never fires once migrated. */
21
+ export function hasUnmigratedLegacyConfig(root) {
22
+ return existsSync(legacyConfigPath(root)) && !existsSync(configPath(root));
23
+ }
24
+ /**
25
+ * Single source of truth for this message so init and scan can't drift into saying it two different
26
+ * ways. Deliberately never auto-reads the legacy file as a fallback, even though the YAML shape is
27
+ * identical -- the rename needs to be an explicit, visible action, not a silent compatibility shim
28
+ * that lets the old filename linger indefinitely.
29
+ */
30
+ export function legacyConfigMessage() {
31
+ return `Found a legacy ${LEGACY_CONFIG_FILENAME} from an older version of SrcLawyer. Rename it to keep your existing settings:\n\n mv ${LEGACY_CONFIG_FILENAME} ${CONFIG_FILENAME}\n\nThen re-run this command. (If you'd rather start fresh instead, delete ${LEGACY_CONFIG_FILENAME} first.)`;
32
+ }
15
33
  export function loadConfig(root) {
16
34
  const path = configPath(root);
17
35
  if (!existsSync(path))
@@ -1,3 +1,3 @@
1
- import type { SrcLawyerConfig } from "../config/config.js";
1
+ import { type SrcLawyerConfig } from "../config/config.js";
2
2
  import type { ProviderResolution } from "./types.js";
3
3
  export declare function resolveProviderForTier(config: SrcLawyerConfig): ProviderResolution;
@@ -1,3 +1,4 @@
1
+ import { CONFIG_FILENAME } from "../config/config.js";
1
2
  import { AnthropicProvider } from "./anthropicProvider.js";
2
3
  export function resolveProviderForTier(config) {
3
4
  const tier = config.tier ?? "free";
@@ -9,7 +10,7 @@ export function resolveProviderForTier(config) {
9
10
  if (!envVar) {
10
11
  return {
11
12
  provider: null,
12
- error: 'Tier is "byok" but no API key environment variable is configured in .privacypolicy.yml. Run "srclawyer init" to set one.',
13
+ error: `Tier is "byok" but no API key environment variable is configured in ${CONFIG_FILENAME}. Run "srclawyer init" to set one.`,
13
14
  };
14
15
  }
15
16
  const apiKey = process.env[envVar];
@@ -27,5 +28,5 @@ export function resolveProviderForTier(config) {
27
28
  error: 'Tier is "managed" but SrcLawyer\'s managed backend is not available yet. Use "byok" with your own Anthropic API key, or "free".',
28
29
  };
29
30
  }
30
- return { provider: null, error: `Unknown tier "${tier}" in .privacypolicy.yml.` };
31
+ return { provider: null, error: `Unknown tier "${tier}" in ${CONFIG_FILENAME}.` };
31
32
  }
@@ -1,3 +1,3 @@
1
1
  import type { ScanResult } from "../engine/types.js";
2
- import type { SrcLawyerConfig } from "../config/config.js";
2
+ import { type SrcLawyerConfig } from "../config/config.js";
3
3
  export declare function generatePolicyMarkdown(result: ScanResult, config: SrcLawyerConfig | null, llmError?: string | null): string;
@@ -1,3 +1,4 @@
1
+ import { CONFIG_FILENAME } from "../config/config.js";
1
2
  import { DATA_CATEGORY_LABELS } from "./dataCategoryLabels.js";
2
3
  import { MANDATORY_DISCLAIMER } from "./disclaimer.js";
3
4
  function locationRef(finding) {
@@ -88,7 +89,7 @@ function renderContextSection(config) {
88
89
  return [
89
90
  "## Scope",
90
91
  "",
91
- "No `.privacypolicy.yml` configuration was found, so this policy was generated without jurisdiction, target-market, or industry context. Run `srclawyer init` and re-run `srclawyer scan` to include that context.",
92
+ `No \`${CONFIG_FILENAME}\` configuration was found, so this policy was generated without jurisdiction, target-market, or industry context. Run \`srclawyer init\` and re-run \`srclawyer scan\` to include that context.`,
92
93
  ].join("\n");
93
94
  }
94
95
  const lines = ["## Scope", ""];
@@ -129,6 +130,9 @@ export function generatePolicyMarkdown(result, config, llmError) {
129
130
  if (categorySections.length > 0) {
130
131
  parts.push(categorySections.join("\n\n"));
131
132
  }
133
+ else if (result.unsupportedStackWarning) {
134
+ parts.push("**Not analyzed:** this codebase's primary language/framework isn't supported yet (see the warning above). This is not a clean-scan result — nothing below should be read as \"no data collection found.\"");
135
+ }
132
136
  else {
133
137
  parts.push("No data-collection patterns were confidently identified in this codebase.");
134
138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "srclawyer",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Static analysis engine that derives privacy policy documentation directly from source code",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -20,11 +20,13 @@
20
20
  "./package.json": "./package.json"
21
21
  },
22
22
  "files": [
23
- "dist"
23
+ "dist",
24
+ "postinstall.cjs"
24
25
  ],
25
26
  "scripts": {
26
27
  "build": "tsc -p tsconfig.json",
27
28
  "prepare": "npm run build",
29
+ "postinstall": "node postinstall.cjs",
28
30
  "dev": "tsx src/cli.ts",
29
31
  "test": "vitest run",
30
32
  "test:corpus": "tsx scripts/scoreCorpus.ts",
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * Advisory-only PATH check, run via the "postinstall" lifecycle hook. Plain, dependency-free
6
+ * CommonJS (not compiled from src/) on purpose: postinstall fires BEFORE "prepare" in npm's
7
+ * lifecycle order, so dist/ doesn't exist yet during this repo's own `npm install` -- a compiled
8
+ * script referencing dist/ would break every contributor's first install. A real, pre-built
9
+ * tarball install (the actual target of this check) always has this file on disk already.
10
+ *
11
+ * IMPORTANT, confirmed empirically: npm (v7+) does NOT share stdio with lifecycle scripts by
12
+ * default -- normal process.stdout/stderr writes from here are silently swallowed for a plain
13
+ * `npm install -g`, visible only with the rarely-used --foreground-scripts flag. Exiting non-zero
14
+ * to force visibility was tried and rejected -- it doesn't just print an ugly error, it makes npm
15
+ * abort the install entirely (the package's files never get placed), which is worse than saying
16
+ * nothing. Writing directly to the controlling terminal device (bypassing the suppressed stdio) is
17
+ * the standard real-world workaround for this exact npm behavior, so that's what this does, with a
18
+ * plain stderr write as the fallback for when there's no TTY to open (CI, non-interactive installs)
19
+ * -- at that point this is genuinely best-effort, not guaranteed.
20
+ *
21
+ * Never fails the install over this: worst case is silence, never a non-zero exit or a thrown
22
+ * error reaching npm's own install process.
23
+ */
24
+
25
+ const fs = require("node:fs");
26
+ const path = require("node:path");
27
+
28
+ function pathImplFor(platform) {
29
+ return platform === "win32" ? path.win32 : path.posix;
30
+ }
31
+
32
+ function globalBinDir(prefix, platform) {
33
+ if (!prefix) return null;
34
+ return platform === "win32" ? prefix : pathImplFor(platform).join(prefix, "bin");
35
+ }
36
+
37
+ function normalize(p, platform) {
38
+ let resolved = pathImplFor(platform).resolve(p).replace(/[\\/]+$/, "");
39
+ if (platform === "win32" || platform === "darwin") resolved = resolved.toLowerCase();
40
+ return resolved;
41
+ }
42
+
43
+ /** Pure, testable: is `dir` present among `pathEnv`'s entries, resolved and normalized? */
44
+ function isDirOnPath(dir, pathEnv, platform) {
45
+ if (!dir) return true; // can't determine the target dir -- don't false-positive a warning
46
+ const target = normalize(dir, platform);
47
+ const delimiter = platform === "win32" ? ";" : ":";
48
+ const entries = (pathEnv || "").split(delimiter).filter(Boolean);
49
+ return entries.some((entry) => normalize(entry, platform) === target);
50
+ }
51
+
52
+ function printWarning(message) {
53
+ // npm doesn't share stdio with lifecycle scripts by default -- a plain stderr write here is
54
+ // silently swallowed for a normal `npm install -g`. Writing straight to the controlling terminal
55
+ // bypasses that. Falls back to stderr (visible with --foreground-scripts, or if something else
56
+ // in the chain does share stdio) when there's no TTY to open -- CI, piped/non-interactive installs.
57
+ try {
58
+ const ttyPath = process.platform === "win32" ? "\\\\.\\CON" : "/dev/tty";
59
+ const fd = fs.openSync(ttyPath, "w");
60
+ try {
61
+ fs.writeSync(fd, message);
62
+ } finally {
63
+ fs.closeSync(fd);
64
+ }
65
+ } catch {
66
+ try {
67
+ process.stderr.write(message);
68
+ } catch {
69
+ // Truly nothing we can do -- advisory only, never throw from here.
70
+ }
71
+ }
72
+ }
73
+
74
+ function main() {
75
+ try {
76
+ if (process.env.npm_config_global !== "true") return; // local/dependency install, or npx -- not our concern
77
+
78
+ const bin = globalBinDir(process.env.npm_config_prefix, process.platform);
79
+ if (isDirOnPath(bin, process.env.PATH, process.platform)) return;
80
+
81
+ printWarning(
82
+ [
83
+ "",
84
+ '⚠ srclawyer installed, but the "srclawyer" command isn’t on your PATH yet.',
85
+ " This is often caused by tools like conda (auto_activate_base) rearranging PATH ahead of",
86
+ " npm's global bin directory. Try opening a new terminal window first.",
87
+ " If that doesn't fix it, see the Troubleshooting section in the README:",
88
+ " https://github.com/SrcLawyer/srclawyer#troubleshooting-command-not-found",
89
+ "",
90
+ ].join("\n")
91
+ );
92
+ } catch {
93
+ // Advisory only -- never let this check itself break an install.
94
+ }
95
+ }
96
+
97
+ main();
98
+
99
+ module.exports = { isDirOnPath, globalBinDir, normalize };