@securityreviewai/securityreview-kit 0.1.11

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,33 @@
1
+ ---
2
+ name: ctm_sync
3
+ description: Command/workflow triggered whenever a threat model is generated or updated. Builds and uploads CTM sync details through security-review-mcp.
4
+ ---
5
+
6
+ # CTM Sync Workflow
7
+
8
+ Configured SRAI project name: `<SRAI_PROJECT_NAME>`
9
+
10
+ When invoked:
11
+
12
+ 0. Verify `create_ai_ide_event` tool exists in `security-review-mcp`
13
+ 1. Read the parent agent context and extract the latest threat model details.
14
+ 2. Build a JSON payload with these exact keys:
15
+ - `workflow_id`
16
+ - `title`
17
+ - `summary`
18
+ - `developer_name` = fetch from git config unless explicity specified by user
19
+ - `developer_email` = fetch from git config unless explicity specified by user
20
+ - `threats_mitigated`
21
+ - `best_practises_achieved`
22
+ - `secure_code_snippets`
23
+ 3. Resolve the configured SRAI project:
24
+ - Call `find_project_by_name` with `name="<SRAI_PROJECT_NAME>"`.
25
+ - If missing, call `list_projects`, then `create_project` if needed.
26
+ 4. Upload the payload using `security-review-mcp`:
27
+ - Use the tool `create_ai_ide_event`
28
+
29
+ ## Output Contract
30
+
31
+ - Never skip upload when a threat model exists.
32
+ - Never invent missing values; use empty strings/arrays if data is unavailable.
33
+ - Return a compact confirmation after upload.
@@ -0,0 +1,78 @@
1
+ ---
2
+ description: ctm_sync agent trigger rule
3
+ alwaysApply: true
4
+ ---
5
+ # Continuous Threat Modeling Sync (`ctm_sync`)
6
+
7
+ **Always synchronize threat models after they are (re)generated.**
8
+
9
+ Any time an AI agent:
10
+ - generates a **new threat model**, or
11
+ - **updates** an existing threat model, or
12
+ - performs a **non-trivial extension** of an existing threat model (e.g., adds new threat scenarios, countermeasures, data flows, or components),
13
+
14
+ it **must immediately call** the `ctm_sync` agent to synchronize the latest threat modeling state.
15
+
16
+ This rule applies regardless of:
17
+ - which methodology is used (PWNISMS, STRIDE, OWASP, etc.),
18
+ - which skill or MCP was used to produce the threat model, and
19
+ - whether the threat model is partial, incremental, or “draft” form.
20
+
21
+ If in doubt — **assume this rule applies**.
22
+
23
+ ---
24
+
25
+ ## When this rule applies
26
+
27
+ This rule is **triggered** whenever the assistant:
28
+
29
+ - **Produces a threat model** in any form, such as:
30
+ - Threat scenarios (even a short list)
31
+ - Data flow diagrams or component diagrams described in text
32
+ - Security objectives or misuse cases
33
+ - Attack trees or abuse cases
34
+ - Structured PWNISMS analysis
35
+ - **Extends or refines** an earlier threat model, e.g.:
36
+ - “Add more threats for the Payments service”
37
+ - “Update the data flow for the auth system”
38
+ - “Refine the mitigations for threat X”
39
+
40
+ This includes both:
41
+ - Threat modeling done **directly in this conversation**, and
42
+ - Threat modeling done by calling **MCP tools or skills** that output threat scenarios, countermeasures, data dictionaries, or other CTM artifacts.
43
+
44
+ ---
45
+
46
+ ## Required behavior
47
+
48
+ **After completing any threat modeling step that produces or modifies threat content, the assistant must:**
49
+
50
+ 1. **Ensure threat modeling output is finalized for this step**
51
+
52
+ - The assistant should finish producing the current threat modeling content (threat scenarios, components, data dictionaries, etc.) in the user-visible message or via tools.
53
+ - The assistant should not call `ctm_sync` *before* the threat model content exists; it must be called **after** threat data for this step is available.
54
+
55
+ 2. **Immediately call the `ctm_sync` agent**
56
+
57
+ - Use the `Task` tool (or equivalent agent launcher) with `subagent_type="ctm_sync"`.
58
+ - Provide a clear, concise description in the `description`/`prompt` fields explaining:
59
+ - What system or component the threat model covers
60
+ - Whether this is a new threat model or an update
61
+ - Where the latest threat content can be found (e.g., “from this conversation” or “from the last threat modeling step”)
62
+
63
+ **Example invocation (conceptual, not literal code):**
64
+
65
+ Use Task tool with:
66
+ - subagent_type: "ctm_sync"
67
+ - description: "Sync latest threat model for <system/component>"
68
+ - prompt: Brief summary of what was threat-modeled, what artifacts were produced (threat scenarios, countermeasures, components, data dictionary, etc.), and that the agent should upload/update CTM data accordingly.
69
+ - readonly: false (it must be allowed to write/update CTM data)
70
+
71
+ 3. Let `ctm_sync` complete its work
72
+ The assistant should wait for the ctm_sync agent to complete (or reach a healthy steady state) before declaring the threat-modeling step fully done.
73
+ If ctm_sync returns errors that can be reasonably fixed (e.g., missing project name), the assistant should fix the issue and retry once, if possible.
74
+
75
+ Acknowledge completion to the user
76
+ After ctm_sync completes, the assistant should briefly tell the user that:
77
+ Threat modeling is finished for this step, and
78
+ The results have been synchronized via ctm_sync (without leaking internal tooling details beyond what is appropriate for the user’s context).
@@ -0,0 +1,84 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { writeText } from '../../utils/fs-helpers.js';
4
+ import {
5
+ getRuleContent,
6
+ getProfileCommandContent,
7
+ getCtmSyncWorkflowContent,
8
+ getCtmSyncTriggerRuleContent,
9
+ getCreateIdeWorkflowCommandContent,
10
+ getThreatModellingSkillContent,
11
+ getHooksContent,
12
+ } from './content.js';
13
+
14
+ function writeCursorRule(filePath, description, content) {
15
+ const action = existsSync(filePath) ? 'updated' : 'created';
16
+ const mdc = `---
17
+ description: ${description}
18
+ alwaysApply: true
19
+ ---
20
+
21
+ ${content}
22
+ `;
23
+
24
+ writeText(filePath, mdc);
25
+ return { filePath, action };
26
+ }
27
+
28
+ function writeCursorCommand(filePath, content) {
29
+ const action = existsSync(filePath) ? 'updated' : 'created';
30
+ writeText(filePath, content);
31
+ return { filePath, action };
32
+ }
33
+
34
+ /**
35
+ * Generate Cursor workspace rules at .cursor/rules/*.mdc
36
+ * Cursor uses .mdc format with YAML front matter.
37
+ */
38
+ export function generate(cwd, options = {}) {
39
+ const baseRulePath = join(cwd, '.cursor', 'rules', 'srai-security-review.mdc');
40
+ const ctmSyncTriggerRulePath = join(cwd, '.cursor', 'rules', 'ctm_sync_rule.mdc');
41
+ const ctmSyncWorkflowPath = join(cwd, '.cursor', 'commands', 'ctm_sync.md');
42
+ const ctmSyncAgentPath = join(cwd, '.cursor', 'agents', 'ctm_sync.md');
43
+ const createIdeWorkflowCommandPath = join(cwd, '.cursor', 'commands', 'create-ide-workflow.md');
44
+ const profileCommandPath = join(cwd, '.cursor', 'commands', 'srai-profile.md');
45
+ const skillPath = join(cwd, '.cursor', 'skills', 'threat-modelling', 'SKILL.md');
46
+ const hooksPath = join(cwd, '.cursor', 'hooks.json');
47
+
48
+ const baseRuleContent = getRuleContent(options);
49
+ const ctmSyncTriggerRuleContent = getCtmSyncTriggerRuleContent(options);
50
+ const ctmSyncWorkflowContent = getCtmSyncWorkflowContent(options);
51
+ const createIdeWorkflowCommandContent = getCreateIdeWorkflowCommandContent(options);
52
+ const profileCommandContent = getProfileCommandContent(options);
53
+ const skillContent = getThreatModellingSkillContent(options);
54
+
55
+ const baseRule = writeCursorRule(
56
+ baseRulePath,
57
+ 'SRAI Security Review gate — consult security-review-mcp before security-relevant code changes',
58
+ baseRuleContent,
59
+ );
60
+
61
+ const ctmSyncTriggerRule = writeCursorCommand(ctmSyncTriggerRulePath, ctmSyncTriggerRuleContent);
62
+ const ctmSyncWorkflow = writeCursorCommand(ctmSyncWorkflowPath, ctmSyncWorkflowContent);
63
+ const ctmSyncAgent = writeCursorCommand(ctmSyncAgentPath, ctmSyncWorkflowContent);
64
+ const createIdeWorkflowCommand = writeCursorCommand(createIdeWorkflowCommandPath, createIdeWorkflowCommandContent);
65
+
66
+ const profileCommand = writeCursorCommand(profileCommandPath, profileCommandContent);
67
+ const skillAction = existsSync(skillPath) ? 'updated' : 'created';
68
+ writeText(skillPath, skillContent);
69
+
70
+ const hooksContent = getHooksContent();
71
+ const hooksAction = existsSync(hooksPath) ? 'updated' : 'created';
72
+ writeText(hooksPath, hooksContent);
73
+
74
+ return [
75
+ { ...baseRule, kind: 'rule' },
76
+ { ...ctmSyncTriggerRule, kind: 'rule' },
77
+ { ...ctmSyncWorkflow, kind: 'command' },
78
+ { ...ctmSyncAgent, kind: 'agent' },
79
+ { ...createIdeWorkflowCommand, kind: 'command' },
80
+ { ...profileCommand, kind: 'command' },
81
+ { filePath: skillPath, action: skillAction, kind: 'skill' },
82
+ { filePath: hooksPath, action: hooksAction, kind: 'hooks' },
83
+ ];
84
+ }
@@ -0,0 +1,13 @@
1
+ import { join } from 'node:path';
2
+ import { upsertSentinelBlock } from '../../utils/fs-helpers.js';
3
+ import { getRuleContent } from './content.js';
4
+
5
+ /**
6
+ * Generate Gemini CLI / Antigravity workspace rule — appends to GEMINI.md
7
+ */
8
+ export function generate(cwd, options = {}) {
9
+ const filePath = join(cwd, 'GEMINI.md');
10
+ const content = getRuleContent(options);
11
+ const action = upsertSentinelBlock(filePath, content);
12
+ return { filePath, action };
13
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "sessionStart": [
5
+ {
6
+ "command": "printf '%s\\n' '{\"additional_context\":\"## MANDATORY SECURITY GATE (Session Policy)\\n\\nFor ANY user request that has security impact (auth, crypto, input handling, secrets, network, data storage, dependencies, new APIs/endpoints, or any code handling untrusted data):\\n\\n**Plan order:**\\n1. **PWNISMS threat modeling (first)** - Run threat modeling as a guide before writing code. Use .cursor/skills/threat-modelling/SKILL.md. Document findings to inform secure implementation.\\n2. **Implement secure code** - Use the threat model findings as a guide to create secure code.\\n3. **ctm_sync (last)** - Push payload to SRAI only after implementation is done. Invoke ctm_sync agent (Task tool, subagent_type=ctm_sync) as the final step. See .cursor/commands/ctm_sync.md and .cursor/rules/ctm-trigger-rule.mdc.\\n\\nNo blocking. No deferral. Threat modeling guides the code; ctm_sync runs last to push to SRAI. the ctm_sync is mandatory , should be a part of your to-do.\"}'",
7
+ "timeout": 5
8
+ }
9
+ ]
10
+ }
11
+ }
@@ -0,0 +1,161 @@
1
+ ---
2
+ name: PWNISMS Threat Modelling
3
+ description: Lightweight, security-first threat modelling workflow for code and architecture tasks. Use it to walk all 7 PWNISMS categories, identify plausible threats, prioritize top risks, and propose concrete mitigations before and during implementation.
4
+ ---
5
+
6
+ # PWNISMS — On-the-Go Threat Modelling
7
+
8
+ For EVERY security-relevant task (feature, bug fix, refactor, infra change, architecture design), run a lightweight threat model with PWNISMS.
9
+
10
+ - Walk through all 7 categories explicitly.
11
+ - If a category is not applicable, state it briefly and move on.
12
+ - Anchor analysis to linked files, diffs, PRs, API specs, and diagrams whenever available.
13
+ - Focus on realistic threats for the current context, not exhaustive attack catalogs.
14
+
15
+ ---
16
+
17
+ ## Inputs to Gather First
18
+
19
+ Collect these quickly before deep analysis:
20
+
21
+ - **Scope**: What is changing (feature, component, service, migration, PR)?
22
+ - **Assets**: What must be protected (PII, credentials, tokens, configs, accounts, workflows)?
23
+ - **Entry points**: How data enters/leaves (HTTP, queues, schedulers, CLI, webhooks, integrations)?
24
+ - **Trust boundaries**: Where data crosses users/services/networks/privilege levels?
25
+
26
+ If the user provided specific code, diffs, or architecture artifacts, prioritize those as primary evidence.
27
+
28
+ ---
29
+
30
+ ## Lightweight Workflow (PWNISMS)
31
+
32
+ 1. **Clarify scope and assumptions**
33
+ - Define the exact unit of analysis.
34
+ - State assumptions explicitly (auth model, deployment boundary, tenant model, etc.).
35
+ 2. **Map assets and flows**
36
+ - List high-value assets and critical data paths.
37
+ - List entry points and exits across trust boundaries.
38
+ 3. **Walk all 7 PWNISMS categories**
39
+ - Identify plausible threats for each category.
40
+ - Keep findings concrete and contextual.
41
+ 4. **Prioritize**
42
+ - Select the top 3-7 risks by impact and likelihood.
43
+ 5. **Mitigate**
44
+ - Propose concrete, implementable controls for each prioritized risk.
45
+ 6. **Summarize residual risk**
46
+ - Call out remaining risk, trade-offs, and follow-up actions.
47
+ - Call out unknowns instead of silently guessing.
48
+
49
+ ---
50
+
51
+ ## The 7 Categories (What to Check)
52
+
53
+ ### P — Product
54
+
55
+ Application and business-logic threats:
56
+
57
+ - Input validation, injection, insecure deserialization.
58
+ - Authorization gaps, privilege escalation, IDOR/BOLA.
59
+ - Business logic abuse, replay/race conditions, unsafe redirects.
60
+ - Error handling that leaks internals.
61
+
62
+ ### W — Workload
63
+
64
+ Compute and infrastructure threats:
65
+
66
+ - Insecure container/runtime posture, over-privileged workload identity.
67
+ - Weak host/orchestrator controls and segmentation.
68
+ - Insecure data storage/backups and DB configuration.
69
+ - Queue/broker abuse and poison-message handling gaps.
70
+
71
+ ### N — Network
72
+
73
+ Network and transport threats:
74
+
75
+ - Missing/weak TLS, insecure service-to-service communication.
76
+ - Exposed ports/endpoints and permissive ingress/egress.
77
+ - Weak segmentation or lateral movement paths.
78
+ - API-layer abuse controls missing (rate limits, request limits, CORS hardening).
79
+
80
+ ### I — IAM (Identity & Access Management)
81
+
82
+ Identity and authorization threats:
83
+
84
+ - Broken authentication controls and token validation.
85
+ - Missing least-privilege RBAC/ABAC.
86
+ - Service-to-service auth gaps.
87
+ - Escalation paths across users, roles, or services.
88
+
89
+ ### S — Secrets
90
+
91
+ Credential and key management threats:
92
+
93
+ - Secrets in code, images, logs, CI output, or defaults.
94
+ - Weak rotation, revocation, or token lifetime policies.
95
+ - Over-shared secrets across components.
96
+ - Missing secret manager/KMS controls.
97
+
98
+ ### M — Monitoring (Logging & Observability)
99
+
100
+ Detection and auditability threats:
101
+
102
+ - Missing logs for auth, authorization, admin/data access events.
103
+ - Sensitive data leakage in logs.
104
+ - Missing alerts for abuse indicators.
105
+ - Incomplete audit trails or weak log integrity.
106
+
107
+ ### S — Supply Chain
108
+
109
+ Dependency and delivery threats:
110
+
111
+ - Unpinned/unverified dependencies and vulnerable packages.
112
+ - Third-party integration trust and scope overreach.
113
+ - CI/CD pipeline leakage or unreviewed build scripts.
114
+ - Unsigned/unprovenanced artifacts, missing SBOM.
115
+ - Treat AI-generated code as untrusted until validated.
116
+
117
+ ---
118
+
119
+ ## Tailor for Architecture / Design Tasks
120
+
121
+ When discussing designs before code exists:
122
+
123
+ - Sketch a mental data flow: actors, data sent/received, storage, processing points.
124
+ - Mark trust boundaries explicitly (client-backend, backend-DB, service-service, cloud-third party).
125
+ - Identify where strong authentication/authorization is mandatory.
126
+ - Identify where encryption in transit and at rest is mandatory.
127
+ - Recommend concrete security patterns:
128
+ - Parameterized queries / ORM for DB access.
129
+ - Centralized authn/authz and role checks.
130
+ - Secrets manager / KMS for credentials and keys.
131
+ - mTLS or signed requests for service-to-service calls.
132
+
133
+ ---
134
+
135
+ ## Security-First Code Generation Rules
136
+
137
+ When implementing code, enforce these baseline controls:
138
+
139
+ 1. Validate and constrain all untrusted input.
140
+ 2. Parameterize all queries and command-like invocations.
141
+ 3. Enforce least privilege for users, services, and workloads.
142
+ 4. Never hardcode secrets; use managed secret stores.
143
+ 5. Encrypt sensitive data in transit and at rest.
144
+ 6. Log security-relevant actions without leaking secrets/PII.
145
+ 7. Pin and verify dependencies and build artifacts.
146
+ 8. Return safe user errors; keep sensitive diagnostics internal.
147
+ 9. Add abuse protections (rate limits, lockouts, throttling) on exposed interfaces.
148
+
149
+ ---
150
+
151
+ ## Post-Generation Checklist
152
+
153
+ Before finalizing output, confirm:
154
+
155
+ - [ ] Scope, assumptions, and trust boundaries were explicit.
156
+ - [ ] All 7 PWNISMS categories were checked (or marked N/A explicitly).
157
+ - [ ] Top risks were prioritized by impact and likelihood.
158
+ - [ ] Mitigations are concrete and actionable.
159
+ - [ ] Residual risk and follow-up actions are stated.
160
+
161
+ If ANY box cannot be checked, you MUST flag the gap to the user with a specific remediation recommendation before finalizing the code.
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: srai-profile
3
+ description: Create and upload a SecurityReviewAI code profile using security-review-mcp
4
+ ---
5
+
6
+ # SRAI Profile Uploader
7
+
8
+ Create a code profile from the current request and upload it to SRAI using `security-review-mcp`.
9
+
10
+ Configured SRAI project name: `<SRAI_PROJECT_NAME>`
11
+
12
+ ## Steps
13
+
14
+ 1. Determine scope from the user prompt.
15
+ - If the request is about a specific capability/module/endpoint, produce a **Feature Code Profile**.
16
+ - If the request is broad, exploratory, or architecture-level, produce a **Codebase Code Profile**.
17
+ 2. Build detailed profile content before any upload call.
18
+ - Include purpose, boundaries, entry points, sensitive data, trust boundaries, integrations, threats, controls, and open risks.
19
+ 3. Resolve the SRAI project.
20
+ - Use the configured project name by default; if it is missing or ambiguous, ask the user to confirm it.
21
+ - Call `find_project_by_name` with `name="<SRAI_PROJECT_NAME>"`.
22
+ - If missing, call `list_projects`; if still missing, call `create_project` with `name="<SRAI_PROJECT_NAME>"`.
23
+ 4. Upload generated markdown content with tool 11: `upload_document`.
24
+ - Use this for raw markdown/text generated in the chat.
25
+ - Use a clear document name such as `feature-code-profile-<feature>.md` or `codebase-code-profile-<repo>.md`.
26
+ 5. Upload files/diagrams with tool 10: `upload_document` when needed.
27
+ - Provide base64 file content and file name.
28
+ - Set `document_type` to `Component Diagram` for architecture diagrams.
29
+ - Set `document_type` to `Uploaded Knowledgebase doc` for other document uploads.
30
+ 6. Confirm the result.
31
+ - Report project name, upload tool used, document name, and that processing is asynchronous.
32
+ - If upload fails, return exact error details and next retry step.
@@ -0,0 +1,13 @@
1
+ import { join } from 'node:path';
2
+ import { upsertSentinelBlock } from '../../utils/fs-helpers.js';
3
+ import { getRuleContent } from './content.js';
4
+
5
+ /**
6
+ * Generate VS Code Copilot instructions — appends to .github/copilot-instructions.md
7
+ */
8
+ export function generate(cwd, options = {}) {
9
+ const filePath = join(cwd, '.github', 'copilot-instructions.md');
10
+ const content = getRuleContent(options);
11
+ const action = upsertSentinelBlock(filePath, content);
12
+ return { filePath, action };
13
+ }
@@ -0,0 +1,13 @@
1
+ import { join } from 'node:path';
2
+ import { writeText } from '../../utils/fs-helpers.js';
3
+ import { getRuleContent } from './content.js';
4
+
5
+ /**
6
+ * Generate Windsurf workspace rule at .windsurf/rules/srai-security-review.md
7
+ */
8
+ export function generate(cwd, options = {}) {
9
+ const filePath = join(cwd, '.windsurf', 'rules', 'srai-security-review.md');
10
+ const content = getRuleContent(options);
11
+ writeText(filePath, content + '\n');
12
+ return filePath;
13
+ }
@@ -0,0 +1,63 @@
1
+ // Shared constants for securityreview-kit
2
+
3
+ export const MCP_SERVER_PACKAGE = 'security-review-mcp';
4
+ export const MCP_SERVER_NAME = 'security-review-mcp';
5
+
6
+ export const ENV_VARS = {
7
+ apiUrl: 'SECURITY_REVIEW_API_URL',
8
+ apiToken: 'SECURITY_REVIEW_API_TOKEN',
9
+ };
10
+
11
+ export const TARGETS = {
12
+ cursor: {
13
+ name: 'Cursor',
14
+ mcpConfigPath: '.cursor/mcp.json',
15
+ rulePath: '.cursor/rules/srai-security-review.mdc',
16
+ detectDirs: ['.cursor'],
17
+ },
18
+ claude: {
19
+ name: 'Claude Code',
20
+ mcpConfigPath: '.claude/settings.json',
21
+ rulePath: 'CLAUDE.md',
22
+ ruleMode: 'append',
23
+ detectDirs: ['.claude'],
24
+ },
25
+ vscode: {
26
+ name: 'VS Code Copilot',
27
+ mcpConfigPath: '.vscode/mcp.json',
28
+ rulePath: '.github/copilot-instructions.md',
29
+ ruleMode: 'append',
30
+ detectDirs: ['.vscode'],
31
+ },
32
+ windsurf: {
33
+ name: 'Windsurf',
34
+ mcpConfigPath: '.windsurf/mcp_config.json',
35
+ rulePath: '.windsurf/rules/srai-security-review.md',
36
+ detectDirs: ['.windsurf'],
37
+ },
38
+ codex: {
39
+ name: 'Codex',
40
+ mcpConfigPath: '.codex/config.toml',
41
+ rulePath: 'AGENTS.md',
42
+ ruleMode: 'append',
43
+ detectDirs: ['.codex'],
44
+ },
45
+ gemini: {
46
+ name: 'Gemini CLI',
47
+ mcpConfigPath: '.gemini/settings.json',
48
+ rulePath: 'GEMINI.md',
49
+ ruleMode: 'append',
50
+ detectDirs: ['.gemini'],
51
+ },
52
+ antigravity: {
53
+ name: 'Antigravity',
54
+ mcpConfigPath: '.gemini/settings.json',
55
+ rulePath: '.agent/rules/srai-security-review.md',
56
+ detectDirs: ['.agent'],
57
+ },
58
+ };
59
+
60
+ export const TARGET_NAMES = Object.keys(TARGETS);
61
+
62
+ export const SENTINEL_START = '<!-- securityreview-kit:start -->';
63
+ export const SENTINEL_END = '<!-- securityreview-kit:end -->';
@@ -0,0 +1,27 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { TARGETS, TARGET_NAMES } from './constants.js';
4
+
5
+ /**
6
+ * Auto-detect which IDE/CLI targets are configured in the given directory
7
+ * by checking for known directories.
8
+ * @param {string} cwd - The directory to scan
9
+ * @returns {string[]} - List of detected target keys
10
+ */
11
+ export function detectTargets(cwd) {
12
+ const detected = [];
13
+ const seen = new Set();
14
+
15
+ for (const key of TARGET_NAMES) {
16
+ const target = TARGETS[key];
17
+ for (const dir of target.detectDirs) {
18
+ const fullPath = join(cwd, dir);
19
+ if (existsSync(fullPath) && !seen.has(key)) {
20
+ detected.push(key);
21
+ seen.add(key);
22
+ }
23
+ }
24
+ }
25
+
26
+ return detected;
27
+ }
@@ -0,0 +1,82 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { SENTINEL_START, SENTINEL_END } from './constants.js';
4
+
5
+ /**
6
+ * Ensure a directory exists, creating parent dirs as needed.
7
+ */
8
+ export function ensureDir(dirPath) {
9
+ if (!existsSync(dirPath)) {
10
+ mkdirSync(dirPath, { recursive: true });
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Read a JSON file, returning null if it doesn't exist or is invalid.
16
+ */
17
+ export function readJson(filePath) {
18
+ try {
19
+ const raw = readFileSync(filePath, 'utf-8');
20
+ return JSON.parse(raw);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Write an object as pretty-printed JSON.
28
+ */
29
+ export function writeJson(filePath, data) {
30
+ ensureDir(dirname(filePath));
31
+ writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
32
+ }
33
+
34
+ /**
35
+ * Read a text file, returning empty string if it doesn't exist.
36
+ */
37
+ export function readText(filePath) {
38
+ try {
39
+ return readFileSync(filePath, 'utf-8');
40
+ } catch {
41
+ return '';
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Write raw text to a file, ensuring parent dirs exist.
47
+ */
48
+ export function writeText(filePath, content) {
49
+ ensureDir(dirname(filePath));
50
+ writeFileSync(filePath, content, 'utf-8');
51
+ }
52
+
53
+ /**
54
+ * Append or replace a sentinel-guarded block in a file.
55
+ * If the file exists and already has the block, it replaces it.
56
+ * If the file exists but doesn't have the block, it appends.
57
+ * If the file doesn't exist, it creates it with just the block.
58
+ */
59
+ export function upsertSentinelBlock(filePath, content) {
60
+ const block = `${SENTINEL_START}\n${content}\n${SENTINEL_END}`;
61
+ const existing = readText(filePath);
62
+
63
+ if (!existing) {
64
+ writeText(filePath, block + '\n');
65
+ return 'created';
66
+ }
67
+
68
+ const startIdx = existing.indexOf(SENTINEL_START);
69
+ const endIdx = existing.indexOf(SENTINEL_END);
70
+
71
+ if (startIdx !== -1 && endIdx !== -1) {
72
+ const before = existing.substring(0, startIdx);
73
+ const after = existing.substring(endIdx + SENTINEL_END.length);
74
+ writeText(filePath, before + block + after);
75
+ return 'updated';
76
+ }
77
+
78
+ // Append with a blank line separator
79
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
80
+ writeText(filePath, existing + separator + block + '\n');
81
+ return 'appended';
82
+ }