patchwork-os 0.2.0-beta.6.canary.37 → 0.2.0-beta.6.canary.41

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.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * auditEnv — statically scan a recipe YAML for {{env.FOO}} references
3
+ * and verify those vars exist in process.env (or an optional .env file).
4
+ *
5
+ * Fills a gap that `recipe preflight` does NOT cover: preflight checks
6
+ * lint/tools/write-steps but silently produces empty string at runtime
7
+ * for missing {{env.FOO}} references. auditEnv catches that statically.
8
+ */
9
+ export interface AuditEnvOptions {
10
+ /** Path to a .env file to check against (workspace-scoped only). */
11
+ envFile?: string;
12
+ /** Required when envFile is provided — used for path-jail check. */
13
+ workspace?: string;
14
+ }
15
+ export interface AuditEnvResult {
16
+ ok: boolean;
17
+ /** Recipe name (from YAML) or the path used to load it. */
18
+ recipe: string;
19
+ /** Env vars referenced in the recipe but not present. */
20
+ missing: string[];
21
+ /** Env vars referenced in the recipe and present. */
22
+ present: string[];
23
+ /** Non-fatal issues (e.g. value looks like a placeholder). */
24
+ warnings: string[];
25
+ }
26
+ /**
27
+ * Scan `recipeRef` (file path or installed recipe name) for {{env.FOO}}
28
+ * references and report which env vars are present / missing.
29
+ *
30
+ * `recipeRef` is resolved exactly like `recipe lint` — absolute/relative file
31
+ * paths are used directly; bare names are looked up under the default recipes
32
+ * dir. We intentionally do NOT re-import the heavy `resolveRecipePath` helper
33
+ * from recipe.ts (would pull in the entire 2 400-line module). Instead we do
34
+ * a minimal two-step: try direct path first, then give a clear error.
35
+ */
36
+ export declare function runAuditEnv(recipeRef: string, options?: AuditEnvOptions): Promise<AuditEnvResult>;
@@ -0,0 +1,202 @@
1
+ /**
2
+ * auditEnv — statically scan a recipe YAML for {{env.FOO}} references
3
+ * and verify those vars exist in process.env (or an optional .env file).
4
+ *
5
+ * Fills a gap that `recipe preflight` does NOT cover: preflight checks
6
+ * lint/tools/write-steps but silently produces empty string at runtime
7
+ * for missing {{env.FOO}} references. auditEnv catches that statically.
8
+ */
9
+ import { existsSync, readFileSync, statSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+ import { parse as parseYaml } from "yaml";
12
+ import { resolveFilePath } from "../tools/utils.js";
13
+ // ── Pattern ──────────────────────────────────────────────────────────────────
14
+ /** Matches {{env.FOO}} and {{env.foo}} references in template strings. */
15
+ const ENV_REF_RE = /\{\{env\.([A-Za-z_][A-Za-z0-9_]*)\}\}/g;
16
+ /**
17
+ * Walk every string value in an arbitrary JSON/YAML-parsed object and collect
18
+ * all unique env var names referenced via {{env.NAME}}.
19
+ */
20
+ function collectEnvRefs(value, seen = new Set()) {
21
+ if (typeof value === "string") {
22
+ for (const m of value.matchAll(ENV_REF_RE)) {
23
+ // m[1] is always defined — the regex has exactly one capture group
24
+ seen.add(m[1]);
25
+ }
26
+ }
27
+ else if (Array.isArray(value)) {
28
+ for (const item of value) {
29
+ collectEnvRefs(item, seen);
30
+ }
31
+ }
32
+ else if (value !== null && typeof value === "object") {
33
+ for (const v of Object.values(value)) {
34
+ collectEnvRefs(v, seen);
35
+ }
36
+ }
37
+ return seen;
38
+ }
39
+ // ── .env file parser ──────────────────────────────────────────────────────────
40
+ /**
41
+ * Parse a simple KEY=VALUE .env file. Lines starting with # and blank lines
42
+ * are ignored. Values may be optionally quoted with ' or ". Does NOT expand
43
+ * variable references — only static values are relevant for presence checks.
44
+ */
45
+ function parseDotEnv(content) {
46
+ const map = new Map();
47
+ for (const rawLine of content.split(/\r?\n/)) {
48
+ const line = rawLine.trim();
49
+ if (!line || line.startsWith("#"))
50
+ continue;
51
+ const eqIdx = line.indexOf("=");
52
+ if (eqIdx === -1)
53
+ continue;
54
+ const key = line.slice(0, eqIdx).trim();
55
+ if (!key)
56
+ continue;
57
+ let val = line.slice(eqIdx + 1).trim();
58
+ // Strip surrounding quotes if present
59
+ if ((val.startsWith('"') && val.endsWith('"')) ||
60
+ (val.startsWith("'") && val.endsWith("'"))) {
61
+ val = val.slice(1, -1);
62
+ }
63
+ map.set(key, val);
64
+ }
65
+ return map;
66
+ }
67
+ // ── Placeholder heuristic ─────────────────────────────────────────────────────
68
+ /** Values that look like placeholders rather than real secrets. */
69
+ const PLACEHOLDER_RE = /^(changeme|replace[-_]?me|todo|fixme|<[^>]+>|\[.*\]|your[-_]?.*here|placeholder|example|xxx+|dummy|fake|test)$/i;
70
+ function looksLikePlaceholder(val) {
71
+ return PLACEHOLDER_RE.test(val.trim());
72
+ }
73
+ // ── Public API ────────────────────────────────────────────────────────────────
74
+ /**
75
+ * Scan `recipeRef` (file path or installed recipe name) for {{env.FOO}}
76
+ * references and report which env vars are present / missing.
77
+ *
78
+ * `recipeRef` is resolved exactly like `recipe lint` — absolute/relative file
79
+ * paths are used directly; bare names are looked up under the default recipes
80
+ * dir. We intentionally do NOT re-import the heavy `resolveRecipePath` helper
81
+ * from recipe.ts (would pull in the entire 2 400-line module). Instead we do
82
+ * a minimal two-step: try direct path first, then give a clear error.
83
+ */
84
+ export async function runAuditEnv(recipeRef, options = {}) {
85
+ // ── 1. Resolve recipe path ─────────────────────────────────────────────────
86
+ const directPath = resolve(recipeRef);
87
+ let recipePath;
88
+ if (existsSync(directPath) && statSync(directPath).isFile()) {
89
+ recipePath = directPath;
90
+ }
91
+ else {
92
+ return {
93
+ ok: false,
94
+ recipe: recipeRef,
95
+ missing: [],
96
+ present: [],
97
+ warnings: [`Recipe file not found: ${recipeRef}`],
98
+ };
99
+ }
100
+ // ── 2. Load + parse YAML ───────────────────────────────────────────────────
101
+ let raw;
102
+ try {
103
+ const text = readFileSync(recipePath, "utf-8");
104
+ raw = parseYaml(text);
105
+ }
106
+ catch (err) {
107
+ const msg = err instanceof Error ? err.message : String(err);
108
+ return {
109
+ ok: false,
110
+ recipe: recipeRef,
111
+ missing: [],
112
+ present: [],
113
+ warnings: [`Failed to parse recipe YAML: ${msg}`],
114
+ };
115
+ }
116
+ // Prefer recipe.name for display; fall back to path.
117
+ const recipeName = raw !== null &&
118
+ typeof raw === "object" &&
119
+ typeof raw.name === "string"
120
+ ? raw.name
121
+ : recipeRef;
122
+ // ── 3. Collect {{env.FOO}} references ─────────────────────────────────────
123
+ const refs = collectEnvRefs(raw);
124
+ if (refs.size === 0) {
125
+ return {
126
+ ok: true,
127
+ recipe: recipeName,
128
+ missing: [],
129
+ present: [],
130
+ warnings: [],
131
+ };
132
+ }
133
+ // ── 4. Build env lookup map ────────────────────────────────────────────────
134
+ // Start with process.env as baseline.
135
+ let envMap = new Map(Object.entries(process.env));
136
+ if (options.envFile) {
137
+ // Security: envFile must stay within workspace.
138
+ if (!options.workspace) {
139
+ return {
140
+ ok: false,
141
+ recipe: recipeName,
142
+ missing: [],
143
+ present: [],
144
+ warnings: [
145
+ "envFile provided but workspace is required for path validation",
146
+ ],
147
+ };
148
+ }
149
+ try {
150
+ resolveFilePath(options.envFile, options.workspace);
151
+ }
152
+ catch (err) {
153
+ const msg = err instanceof Error ? err.message : String(err);
154
+ return {
155
+ ok: false,
156
+ recipe: recipeName,
157
+ missing: [],
158
+ present: [],
159
+ warnings: [`envFile path rejected: ${msg}`],
160
+ };
161
+ }
162
+ const envFilePath = resolve(options.workspace, options.envFile);
163
+ if (!existsSync(envFilePath)) {
164
+ return {
165
+ ok: false,
166
+ recipe: recipeName,
167
+ missing: [],
168
+ present: [],
169
+ warnings: [`envFile not found: ${options.envFile}`],
170
+ };
171
+ }
172
+ const dotEnvContent = readFileSync(envFilePath, "utf-8");
173
+ const dotEnvMap = parseDotEnv(dotEnvContent);
174
+ // envFile entries supplement (and override) process.env entries.
175
+ envMap = new Map([...envMap, ...dotEnvMap]);
176
+ }
177
+ // ── 5. Split refs into present / missing ──────────────────────────────────
178
+ const missing = [];
179
+ const present = [];
180
+ const warnings = [];
181
+ for (const name of Array.from(refs).sort()) {
182
+ const val = envMap.get(name);
183
+ if (val === undefined || val === null) {
184
+ missing.push(name);
185
+ }
186
+ else {
187
+ present.push(name);
188
+ // Warn if the value looks like an unfilled placeholder.
189
+ if (looksLikePlaceholder(val)) {
190
+ warnings.push(`${name} is set but value looks like a placeholder ("${val}")`);
191
+ }
192
+ }
193
+ }
194
+ return {
195
+ ok: missing.length === 0,
196
+ recipe: recipeName,
197
+ missing,
198
+ present,
199
+ warnings,
200
+ };
201
+ }
202
+ //# sourceMappingURL=auditEnv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auditEnv.js","sourceRoot":"","sources":["../../src/commands/auditEnv.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `patchwork doctor` CLI verb.
3
+ *
4
+ * Runs a subset of bridge health checks that are safe without a live bridge
5
+ * connection (no extensionClient, no ProbeResults). Reports workspace path,
6
+ * git binary, lock file, and automation policy readability.
7
+ *
8
+ * No changes to src/index.ts — wire routing separately.
9
+ */
10
+ export interface DoctorOptions {
11
+ workspace?: string;
12
+ port?: number;
13
+ automationPolicyPath?: string;
14
+ json?: boolean;
15
+ }
16
+ export interface DoctorResult {
17
+ ok: boolean;
18
+ checks: Array<{
19
+ name: string;
20
+ status: "ok" | "warn" | "fail";
21
+ detail?: string;
22
+ suggestion?: string;
23
+ }>;
24
+ }
25
+ /**
26
+ * Run CLI-safe bridge health checks and return a structured result.
27
+ *
28
+ * Maps `CheckResult` (which uses `"error"` for failures) to the public
29
+ * `DoctorResult` shape (which uses `"fail"`). Warns are preserved as `"warn"`.
30
+ * Skipped checks are surfaced as `"ok"` (they are non-issues from the CLI
31
+ * perspective).
32
+ *
33
+ * `ok` is `true` when no check has `status === "fail"`.
34
+ */
35
+ export declare function runDoctor(options?: DoctorOptions): Promise<DoctorResult>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `patchwork doctor` CLI verb.
3
+ *
4
+ * Runs a subset of bridge health checks that are safe without a live bridge
5
+ * connection (no extensionClient, no ProbeResults). Reports workspace path,
6
+ * git binary, lock file, and automation policy readability.
7
+ *
8
+ * No changes to src/index.ts — wire routing separately.
9
+ */
10
+ import { runBridgeHealthChecks } from "../tools/bridgeDoctor.js";
11
+ /**
12
+ * Run CLI-safe bridge health checks and return a structured result.
13
+ *
14
+ * Maps `CheckResult` (which uses `"error"` for failures) to the public
15
+ * `DoctorResult` shape (which uses `"fail"`). Warns are preserved as `"warn"`.
16
+ * Skipped checks are surfaced as `"ok"` (they are non-issues from the CLI
17
+ * perspective).
18
+ *
19
+ * `ok` is `true` when no check has `status === "fail"`.
20
+ */
21
+ export async function runDoctor(options = {}) {
22
+ const workspace = options.workspace ?? process.cwd();
23
+ const raw = await runBridgeHealthChecks(workspace, {
24
+ port: options.port,
25
+ automationPolicyPath: options.automationPolicyPath,
26
+ });
27
+ const checks = raw.map((c) => {
28
+ let status;
29
+ if (c.status === "error") {
30
+ status = "fail";
31
+ }
32
+ else if (c.status === "warn") {
33
+ status = "warn";
34
+ }
35
+ else {
36
+ // "ok" | "skip" — both treated as non-failing from CLI perspective
37
+ status = "ok";
38
+ }
39
+ const entry = { name: c.name, status };
40
+ if (c.detail !== undefined)
41
+ entry.detail = c.detail;
42
+ if (c.suggestion !== undefined)
43
+ entry.suggestion = c.suggestion;
44
+ return entry;
45
+ });
46
+ return {
47
+ ok: checks.every((c) => c.status !== "fail"),
48
+ checks,
49
+ };
50
+ }
51
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAmBjE;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,UAAyB,EAAE;IAE3B,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAErD,MAAM,GAAG,GAAG,MAAM,qBAAqB,CAAC,SAAS,EAAE;QACjD,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,oBAAoB,EAAE,OAAO,CAAC,oBAAoB;KACnD,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC3B,IAAI,MAA8B,CAAC;QACnC,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;YACzB,MAAM,GAAG,MAAM,CAAC;QAClB,CAAC;aAAM,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC/B,MAAM,GAAG,MAAM,CAAC;QAClB,CAAC;aAAM,CAAC;YACN,mEAAmE;YACnE,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;QAED,MAAM,KAAK,GAAmC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QACvE,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS;YAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;QACpD,IAAI,CAAC,CAAC,UAAU,KAAK,SAAS;YAAE,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC;QAChE,OAAO,KAAK,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC;QAC5C,MAAM;KACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `patchwork shadow-scan` CLI — replay historical run data through
3
+ * the destructive-tool classifier and report which runs would be reclassified.
4
+ *
5
+ * Default runs source: ~/.claude/ide/runs.jsonl (outside any workspace —
6
+ * do NOT validate through resolveFilePath). If --runs-file is supplied, it
7
+ * IS workspace-scoped and validated through resolveFilePath.
8
+ */
9
+ import { type RunRecord } from "../testing/shadowRun.js";
10
+ export interface ShadowScanCliOptions {
11
+ /** ISO date string or relative like "24h", "7d". Default: last 7 days. */
12
+ since?: string;
13
+ limit?: number;
14
+ /** Override default ~/.claude/ide/runs.jsonl path. Workspace-scoped. */
15
+ runsFile?: string;
16
+ /** Output JSON instead of human-readable text. */
17
+ json?: boolean;
18
+ /** Workspace root for resolveFilePath (required if runsFile is set). */
19
+ workspace?: string;
20
+ }
21
+ /**
22
+ * Parse a relative duration string like "24h" or "7d" into a Date that many
23
+ * milliseconds in the past. If the string is not a recognised relative form,
24
+ * fall back to `new Date(str)` (ISO 8601 parse).
25
+ *
26
+ * Exported so tests can call it directly.
27
+ */
28
+ export declare function parseSinceDuration(str: string): Date;
29
+ /**
30
+ * Parse JSONL content into RunRecord[]. Malformed lines are skipped with a
31
+ * stderr warning. Exported so tests can call it directly.
32
+ */
33
+ export declare function parseRunsFile(content: string): RunRecord[];
34
+ export declare function runShadowScanCli(options?: ShadowScanCliOptions): Promise<void>;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * `patchwork shadow-scan` CLI — replay historical run data through
3
+ * the destructive-tool classifier and report which runs would be reclassified.
4
+ *
5
+ * Default runs source: ~/.claude/ide/runs.jsonl (outside any workspace —
6
+ * do NOT validate through resolveFilePath). If --runs-file is supplied, it
7
+ * IS workspace-scoped and validated through resolveFilePath.
8
+ */
9
+ import fs from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import { destructiveToolClassifier, runShadowScan, } from "../testing/shadowRun.js";
13
+ import { resolveFilePath } from "../tools/utils.js";
14
+ /** Max bytes the runs JSONL is allowed to be before we skip the read. */
15
+ const MAX_RUNS_BYTES = 1_048_576; // 1 MB
16
+ /**
17
+ * Parse a relative duration string like "24h" or "7d" into a Date that many
18
+ * milliseconds in the past. If the string is not a recognised relative form,
19
+ * fall back to `new Date(str)` (ISO 8601 parse).
20
+ *
21
+ * Exported so tests can call it directly.
22
+ */
23
+ export function parseSinceDuration(str) {
24
+ const relMatch = /^(\d+)(h|d)$/.exec(str.trim());
25
+ if (relMatch) {
26
+ const amount = parseInt(relMatch[1], 10);
27
+ const unit = relMatch[2];
28
+ const ms = unit === "h" ? amount * 3_600_000 : amount * 86_400_000;
29
+ return new Date(Date.now() - ms);
30
+ }
31
+ const parsed = new Date(str);
32
+ if (Number.isNaN(parsed.getTime())) {
33
+ throw new Error(`Invalid since value: "${str}". Use ISO 8601 or relative like "24h", "7d".`);
34
+ }
35
+ return parsed;
36
+ }
37
+ /**
38
+ * Parse JSONL content into RunRecord[]. Malformed lines are skipped with a
39
+ * stderr warning. Exported so tests can call it directly.
40
+ */
41
+ export function parseRunsFile(content) {
42
+ const records = [];
43
+ const lines = content.split("\n");
44
+ for (let i = 0; i < lines.length; i++) {
45
+ const line = (lines[i] ?? "").trim();
46
+ if (line.length === 0)
47
+ continue;
48
+ try {
49
+ const parsed = JSON.parse(line);
50
+ if (typeof parsed === "object" &&
51
+ parsed !== null &&
52
+ !Array.isArray(parsed)) {
53
+ records.push(parsed);
54
+ }
55
+ else {
56
+ process.stderr.write(`[shadow-scan] warn: line ${i + 1} is not an object — skipped\n`);
57
+ }
58
+ }
59
+ catch {
60
+ process.stderr.write(`[shadow-scan] warn: line ${i + 1} is malformed JSON — skipped\n`);
61
+ }
62
+ }
63
+ return records;
64
+ }
65
+ function defaultRunsPath() {
66
+ return path.join(os.homedir(), ".claude", "ide", "runs.jsonl");
67
+ }
68
+ function buildLoadPastRuns(runsPath) {
69
+ return async () => {
70
+ let stat;
71
+ try {
72
+ stat = fs.statSync(runsPath);
73
+ }
74
+ catch (err) {
75
+ // File absent → no runs to scan
76
+ if (err.code === "ENOENT") {
77
+ return [];
78
+ }
79
+ throw err;
80
+ }
81
+ if (stat.size > MAX_RUNS_BYTES) {
82
+ process.stderr.write(`[shadow-scan] warn: ${runsPath} is ${stat.size} bytes (> 1 MB limit) — skipping read\n`);
83
+ return [];
84
+ }
85
+ const content = fs.readFileSync(runsPath, "utf8");
86
+ return parseRunsFile(content);
87
+ };
88
+ }
89
+ function printHumanReadable(result) {
90
+ process.stdout.write(`Scanned: ${result.scanned}\n`);
91
+ process.stdout.write(`Reclassified: ${result.reclassified}\n`);
92
+ if (result.reclassified === 0) {
93
+ process.stdout.write("No runs would be reclassified.\n");
94
+ return;
95
+ }
96
+ process.stdout.write("\n");
97
+ for (const c of result.classifications) {
98
+ if (!c.reclassified)
99
+ continue;
100
+ const reason = c.reason ?? "no reason given";
101
+ process.stdout.write(`[REVIEW] ${c.recipeName} / ${c.toolName} — ${reason}\n`);
102
+ }
103
+ }
104
+ export async function runShadowScanCli(options = {}) {
105
+ // Parse --since
106
+ let since;
107
+ if (options.since !== undefined) {
108
+ since = parseSinceDuration(options.since);
109
+ }
110
+ else {
111
+ // Default: last 7 days
112
+ since = parseSinceDuration("7d");
113
+ }
114
+ // Resolve runs file path
115
+ let runsPath;
116
+ if (options.runsFile !== undefined) {
117
+ // Explicitly provided — validate via resolveFilePath (workspace-scoped)
118
+ const workspace = options.workspace ?? process.cwd();
119
+ runsPath = resolveFilePath(options.runsFile, workspace);
120
+ }
121
+ else {
122
+ // Default path is outside any workspace — do NOT use resolveFilePath
123
+ runsPath = defaultRunsPath();
124
+ }
125
+ const loadPastRuns = buildLoadPastRuns(runsPath);
126
+ const result = await runShadowScan({
127
+ loadPastRuns,
128
+ classifier: destructiveToolClassifier,
129
+ since,
130
+ limit: options.limit,
131
+ });
132
+ if (options.json) {
133
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
134
+ }
135
+ else {
136
+ printHumanReadable(result);
137
+ }
138
+ if (result.reclassified > 0) {
139
+ process.exitCode = 1;
140
+ }
141
+ }
142
+ //# sourceMappingURL=shadowScan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shadowScan.js","sourceRoot":"","sources":["../../src/commands/shadowScan.ts"],"names":[],"mappings":"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"}