guardlink 1.3.0 → 1.4.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/CHANGELOG.md +44 -0
- package/README.md +43 -1
- package/dist/agents/launcher.d.ts +1 -1
- package/dist/agents/launcher.js +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +300 -54
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +38 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/suggest.d.ts +1 -0
- package/dist/mcp/suggest.d.ts.map +1 -1
- package/dist/mcp/suggest.js +1 -0
- package/dist/mcp/suggest.js.map +1 -1
- package/dist/parser/parse-project.d.ts.map +1 -1
- package/dist/parser/parse-project.js +103 -0
- package/dist/parser/parse-project.js.map +1 -1
- package/dist/tui/commands.d.ts +3 -0
- package/dist/tui/commands.d.ts.map +1 -1
- package/dist/tui/commands.js +297 -39
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +17 -1
- package/dist/tui/index.js.map +1 -1
- package/dist/types/index.d.ts +39 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/workspace/index.d.ts +12 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +9 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/link.d.ts +91 -0
- package/dist/workspace/link.d.ts.map +1 -0
- package/dist/workspace/link.js +581 -0
- package/dist/workspace/link.js.map +1 -0
- package/dist/workspace/merge.d.ts +104 -0
- package/dist/workspace/merge.d.ts.map +1 -0
- package/dist/workspace/merge.js +752 -0
- package/dist/workspace/merge.js.map +1 -0
- package/dist/workspace/metadata.d.ts +34 -0
- package/dist/workspace/metadata.d.ts.map +1 -0
- package/dist/workspace/metadata.js +181 -0
- package/dist/workspace/metadata.js.map +1 -0
- package/dist/workspace/types.d.ts +134 -0
- package/dist/workspace/types.d.ts.map +1 -0
- package/dist/workspace/types.js +12 -0
- package/dist/workspace/types.js.map +1 -0
- package/package.json +1 -1
package/dist/tui/commands.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* @handles secrets on #tui -- "Processes and stores API keys via /model"
|
|
22
22
|
*/
|
|
23
23
|
import { resolve, basename } from 'node:path';
|
|
24
|
-
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
24
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
25
25
|
import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures, clearAnnotations } from '../parser/index.js';
|
|
26
26
|
import { initProject, detectProject, promptAgentSelection, syncAgentFiles } from '../init/index.js';
|
|
27
27
|
import { generateReport } from '../report/index.js';
|
|
@@ -35,6 +35,7 @@ import { resolveLLMConfig, saveTuiConfig, loadTuiConfig } from './config.js';
|
|
|
35
35
|
import { AGENTS, parseAgentFlag, launchAgent, launchAgentInline, copyToClipboard, buildAnnotatePrompt } from '../agents/index.js';
|
|
36
36
|
import { describeConfigSource } from '../agents/config.js';
|
|
37
37
|
import { getReviewableExposures, applyReviewAction, summarizeReview } from '../review/index.js';
|
|
38
|
+
import { loadWorkspaceConfig, linkProject, addToWorkspace, removeFromWorkspace, mergeReports, formatMergeSummary, diffMergedReports } from '../workspace/index.js';
|
|
38
39
|
// ─── Shared context ──────────────────────────────────────────────────
|
|
39
40
|
/** Prompt user to pick an agent interactively (TUI only) */
|
|
40
41
|
async function pickAgent(ctx) {
|
|
@@ -86,6 +87,7 @@ export function cmdHelp() {
|
|
|
86
87
|
['/assets', 'Asset tree with threat/control counts'],
|
|
87
88
|
['/files', 'Annotated file tree with exposure counts'],
|
|
88
89
|
['/view <file>', 'Show all annotations in a file with code context'],
|
|
90
|
+
['/unannotated', 'List source files with no annotations'],
|
|
89
91
|
['', ''],
|
|
90
92
|
['/threat-report <fw>', 'AI threat report (stride|dread|pasta|attacker|rapid|general|custom)'],
|
|
91
93
|
['/threat-reports', 'List saved AI threat reports'],
|
|
@@ -101,6 +103,10 @@ export function cmdHelp() {
|
|
|
101
103
|
['/diff [ref]', 'Compare model against a git ref (default: HEAD~1)'],
|
|
102
104
|
['/sarif [-o file]', 'Export SARIF 2.1.0 for GitHub / VS Code'],
|
|
103
105
|
['', ''],
|
|
106
|
+
['/workspace', 'Show workspace config and linked repos'],
|
|
107
|
+
['/link <repos...>', 'Link repos into a workspace (--add / --remove)'],
|
|
108
|
+
['/merge <files...>', 'Merge report JSONs into unified dashboard'],
|
|
109
|
+
['', ''],
|
|
104
110
|
['/gal', 'GAL annotation language guide'],
|
|
105
111
|
['/help', 'This help'],
|
|
106
112
|
['/quit', 'Exit'],
|
|
@@ -131,112 +137,130 @@ export function cmdGal() {
|
|
|
131
137
|
console.log(D(' Annotations live in source code comments. GuardLink parses'));
|
|
132
138
|
console.log(D(' them to build a live threat model from your codebase.'));
|
|
133
139
|
console.log('');
|
|
134
|
-
console.log(D(' Syntax: @verb subject [preposition object] [
|
|
140
|
+
console.log(D(' Syntax: @verb subject [preposition object] [-- "description"]'));
|
|
135
141
|
console.log('');
|
|
136
142
|
// ── DEFINITIONS ──────────────────────────────────────────────────
|
|
137
143
|
console.log(H(' ── Definitions ─────────────────────────────────────────────'));
|
|
138
144
|
console.log('');
|
|
139
|
-
console.log(` ${V('@asset')} ${K('<path>')} ${D('[
|
|
145
|
+
console.log(` ${V('@asset')} ${K('<path>')} ${D('[-- "description"]')}`);
|
|
140
146
|
console.log(D(' Declare a named asset (component, service, data store).'));
|
|
141
147
|
console.log(D(' Path uses dot notation for hierarchy.'));
|
|
142
|
-
console.log(EX(' // @asset api.auth.token_store
|
|
148
|
+
console.log(EX(' // @asset api.auth.token_store -- "Stores JWT refresh tokens"'));
|
|
143
149
|
console.log(EX(' // @asset db.users'));
|
|
144
150
|
console.log('');
|
|
145
|
-
console.log(` ${V('@threat')} ${K('<name>')} ${D('[
|
|
146
|
-
console.log(D(' Declare a named threat. Severity
|
|
147
|
-
console.log(EX(' // @threat SQL Injection
|
|
148
|
-
console.log(EX(' // @threat Token Theft
|
|
151
|
+
console.log(` ${V('@threat')} ${K('<name>')} ${D('(#id)')} ${D('[critical|high|medium|low]')} ${D('[ext-refs]')} ${D('[-- "description"]')}`);
|
|
152
|
+
console.log(D(' Declare a named threat. Severity in brackets: [P0]=[critical] [P1]=[high] [P2]=[medium] [P3]=[low].'));
|
|
153
|
+
console.log(EX(' // @threat SQL Injection (#sql-inj) [high] cwe:CWE-89 -- "Unsanitized input reaches DB"'));
|
|
154
|
+
console.log(EX(' // @threat Token Theft [P0]'));
|
|
149
155
|
console.log('');
|
|
150
|
-
console.log(` ${V('@control')} ${K('<name>')} ${D('[
|
|
156
|
+
console.log(` ${V('@control')} ${K('<name>')} ${D('(#id)')} ${D('[-- "description"]')}`);
|
|
151
157
|
console.log(D(' Declare a security control (mitigation mechanism).'));
|
|
152
|
-
console.log(EX(' // @control Input Validation
|
|
158
|
+
console.log(EX(' // @control Input Validation (#input-val) -- "Sanitize all user-supplied strings"'));
|
|
153
159
|
console.log(EX(' // @control Rate Limiting'));
|
|
154
160
|
console.log('');
|
|
155
161
|
// ── RELATIONSHIPS ─────────────────────────────────────────────────
|
|
156
162
|
console.log(H(' ── Relationships ───────────────────────────────────────────'));
|
|
157
163
|
console.log('');
|
|
158
|
-
console.log(` ${V('@exposes')} ${K('<asset>')} ${D('to')} ${K('<threat>')} ${D('[severity
|
|
164
|
+
console.log(` ${V('@exposes')} ${K('<asset>')} ${D('to')} ${K('<threat>')} ${D('[severity]')} ${D('[ext-refs]')} ${D('[-- "description"]')}`);
|
|
159
165
|
console.log(D(' Mark an asset as exposed to a threat at this code location.'));
|
|
160
166
|
console.log(D(' This is the primary annotation — every exposure creates a finding.'));
|
|
161
|
-
console.log(EX(' // @exposes api.auth to SQL Injection
|
|
162
|
-
console.log(EX(' // @exposes db.users to Token Theft
|
|
167
|
+
console.log(EX(' // @exposes api.auth to SQL Injection [high] cwe:CWE-89'));
|
|
168
|
+
console.log(EX(' // @exposes db.users to Token Theft [critical] -- "No token rotation"'));
|
|
163
169
|
console.log('');
|
|
164
|
-
console.log(` ${V('@mitigates')} ${K('<asset>')} ${D('against')} ${K('<threat>')} ${D('[
|
|
170
|
+
console.log(` ${V('@mitigates')} ${K('<asset>')} ${D('against')} ${K('<threat>')} ${D('[using')} ${K('<control>')}${D(']')} ${D('[-- "description"]')}`);
|
|
165
171
|
console.log(D(' Mark that a control mitigates a threat on an asset.'));
|
|
166
172
|
console.log(D(' Closes the exposure — removes it from open findings.'));
|
|
167
|
-
console.log(
|
|
168
|
-
console.log(EX(' // @mitigates
|
|
173
|
+
console.log(D(' "using" is the primary keyword; "with" also accepted.'));
|
|
174
|
+
console.log(EX(' // @mitigates api.auth against SQL Injection using Input Validation'));
|
|
175
|
+
console.log(EX(' // @mitigates db.users against Token Theft -- "Rotation implemented in v2"'));
|
|
169
176
|
console.log('');
|
|
170
|
-
console.log(` ${V('@accepts')} ${K('<threat>')} ${D('on')} ${K('<asset>')} ${D('[
|
|
177
|
+
console.log(` ${V('@accepts')} ${K('<threat>')} ${D('on')} ${K('<asset>')} ${D('[-- "reason"]')}`);
|
|
171
178
|
console.log(D(' Explicitly accept a risk. Removes it from open findings.'));
|
|
172
179
|
console.log(D(' Use when the risk is known and intentionally not mitigated.'));
|
|
173
|
-
console.log(EX(' // @accepts Timing Attack on api.auth
|
|
180
|
+
console.log(EX(' // @accepts Timing Attack on api.auth -- "Acceptable for current threat model"'));
|
|
174
181
|
console.log('');
|
|
175
|
-
console.log(` ${V('@transfers')} ${K('<threat>')} ${D('from')} ${K('<source>')} ${D('to')} ${K('<target>')} ${D('[
|
|
182
|
+
console.log(` ${V('@transfers')} ${K('<threat>')} ${D('from')} ${K('<source>')} ${D('to')} ${K('<target>')} ${D('[-- "description"]')}`);
|
|
176
183
|
console.log(D(' Transfer responsibility for a threat to another asset/team.'));
|
|
177
|
-
console.log(EX(' // @transfers DDoS from api.gateway to cdn.cloudflare
|
|
184
|
+
console.log(EX(' // @transfers DDoS from api.gateway to cdn.cloudflare -- "Handled by CDN layer"'));
|
|
178
185
|
console.log('');
|
|
179
186
|
// ── DATA FLOWS ────────────────────────────────────────────────────
|
|
180
187
|
console.log(H(' ── Data Flows & Boundaries ─────────────────────────────────'));
|
|
181
188
|
console.log('');
|
|
182
|
-
console.log(` ${V('@flows')} ${K('<source>')} ${D('
|
|
189
|
+
console.log(` ${V('@flows')} ${K('<source>')} ${D('->')} ${K('<target>')} ${D('[via')} ${K('<mechanism>')}${D(']')} ${D('[-- "description"]')}`);
|
|
183
190
|
console.log(D(' Document data movement between components.'));
|
|
184
191
|
console.log(D(' Appears in the Data Flow Diagram.'));
|
|
185
|
-
console.log(EX(' // @flows api.auth
|
|
186
|
-
console.log(EX(' // @flows mobile.app
|
|
192
|
+
console.log(EX(' // @flows api.auth -> db.users via TLS 1.3'));
|
|
193
|
+
console.log(EX(' // @flows mobile.app -> api.gateway via HTTPS -- "User credentials"'));
|
|
187
194
|
console.log('');
|
|
188
|
-
console.log(` ${V('@boundary')} ${K('<asset_a>')} ${D('and')} ${K('<asset_b>')} ${D('[
|
|
195
|
+
console.log(` ${V('@boundary')} ${K('<asset_a>')} ${D('and')} ${K('<asset_b>')} ${D('(#id)')} ${D('[-- "description"]')}`);
|
|
189
196
|
console.log(D(' Declare a trust boundary between two assets.'));
|
|
190
197
|
console.log(D(' Groups assets in the Data Flow Diagram.'));
|
|
191
|
-
console.log(
|
|
192
|
-
console.log(EX(' // @boundary api.gateway
|
|
198
|
+
console.log(D(' Alternate: @boundary between A and B or @boundary A | B'));
|
|
199
|
+
console.log(EX(' // @boundary internet and api.gateway (#edge) -- "Public-facing edge"'));
|
|
200
|
+
console.log(EX(' // @boundary api.gateway | db.users -- "Internal network boundary"'));
|
|
193
201
|
console.log('');
|
|
194
202
|
// ── LIFECYCLE ─────────────────────────────────────────────────────
|
|
195
203
|
console.log(H(' ── Lifecycle & Governance ──────────────────────────────────'));
|
|
196
204
|
console.log('');
|
|
197
|
-
console.log(` ${V('@handles')} ${K('<classification>')} ${D('on')} ${K('<asset>')} ${D('[
|
|
205
|
+
console.log(` ${V('@handles')} ${K('<classification>')} ${D('on')} ${K('<asset>')} ${D('[-- "description"]')}`);
|
|
198
206
|
console.log(D(' Declare data classification handled by an asset.'));
|
|
199
207
|
console.log(D(' Classifications: pii phi financial secrets internal public'));
|
|
200
|
-
console.log(EX(' // @handles pii on db.users
|
|
208
|
+
console.log(EX(' // @handles pii on db.users -- "Stores name, email, phone"'));
|
|
201
209
|
console.log(EX(' // @handles secrets on api.auth.token_store'));
|
|
202
210
|
console.log('');
|
|
203
|
-
console.log(` ${V('@owns')} ${K('<owner>')} ${K('<asset>')} ${D('[
|
|
211
|
+
console.log(` ${V('@owns')} ${K('<owner>')} ${D('for')} ${K('<asset>')} ${D('[-- "description"]')}`);
|
|
204
212
|
console.log(D(' Assign ownership of an asset to a team or person.'));
|
|
205
|
-
console.log(EX(' // @owns platform-team api.auth'));
|
|
213
|
+
console.log(EX(' // @owns platform-team for api.auth'));
|
|
206
214
|
console.log('');
|
|
207
|
-
console.log(` ${V('@validates')} ${K('<control>')} ${D('
|
|
215
|
+
console.log(` ${V('@validates')} ${K('<control>')} ${D('for')} ${K('<asset>')} ${D('[-- "description"]')}`);
|
|
208
216
|
console.log(D(' Assert that a control has been validated/tested on an asset.'));
|
|
209
|
-
console.log(EX(' // @validates Input Validation
|
|
217
|
+
console.log(EX(' // @validates Input Validation for api.auth -- "Pen-tested 2024-Q3"'));
|
|
210
218
|
console.log('');
|
|
211
|
-
console.log(` ${V('@audit')} ${K('<asset>')} ${D('[
|
|
219
|
+
console.log(` ${V('@audit')} ${K('<asset>')} ${D('[-- "description"]')}`);
|
|
212
220
|
console.log(D(' Mark that this code path is an audit trail point.'));
|
|
213
|
-
console.log(EX(' // @audit db.users
|
|
221
|
+
console.log(EX(' // @audit db.users -- "All writes logged to audit_log table"'));
|
|
214
222
|
console.log('');
|
|
215
|
-
console.log(` ${V('@assumes')} ${K('<asset>')} ${D('[
|
|
223
|
+
console.log(` ${V('@assumes')} ${K('<asset>')} ${D('[-- "description"]')}`);
|
|
216
224
|
console.log(D(' Document a security assumption about an asset.'));
|
|
217
|
-
console.log(EX(' // @assumes api.gateway
|
|
225
|
+
console.log(EX(' // @assumes api.gateway -- "Upstream WAF filters malformed requests"'));
|
|
218
226
|
console.log('');
|
|
219
|
-
console.log(` ${V('@comment')} ${D('[
|
|
227
|
+
console.log(` ${V('@comment')} ${D('[-- "description"]')}`);
|
|
220
228
|
console.log(D(' Free-form developer security note (no structural effect).'));
|
|
221
|
-
console.log(EX(' // @comment
|
|
229
|
+
console.log(EX(' // @comment -- "TODO — add rate limiting before v2 launch"'));
|
|
222
230
|
console.log('');
|
|
223
231
|
// ── SHIELD BLOCKS ─────────────────────────────────────────────────
|
|
224
232
|
console.log(H(' ── Shield Blocks ───────────────────────────────────────────'));
|
|
225
233
|
console.log('');
|
|
234
|
+
console.log(` ${V('@shield')} ${D('[-- "reason"]')}`);
|
|
235
|
+
console.log(D(' Single-line marker for a security-sensitive code point.'));
|
|
236
|
+
console.log(EX(' // @shield -- "Crypto key derivation — do not refactor without review"'));
|
|
237
|
+
console.log('');
|
|
226
238
|
console.log(` ${V('@shield:begin')} ${D('/')} ${V('@shield:end')}`);
|
|
227
239
|
console.log(D(' Wrap a code block to mark it as security-sensitive.'));
|
|
228
240
|
console.log(D(' GuardLink will flag unannotated symbols inside the block.'));
|
|
229
|
-
console.log(EX(' // @shield:begin'));
|
|
241
|
+
console.log(EX(' // @shield:begin -- "Auth verification block"'));
|
|
230
242
|
console.log(EX(' function verifyToken(token: string) { ... }'));
|
|
231
243
|
console.log(EX(' // @shield:end'));
|
|
232
244
|
console.log('');
|
|
245
|
+
// ── EXTERNAL REFERENCES ─────────────────────────────────────────
|
|
246
|
+
console.log(H(' ── External References ─────────────────────────────────────'));
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log(D(' Append space-separated refs after severity on @threat and @exposes:'));
|
|
249
|
+
console.log(EX(' cwe:CWE-89 owasp:A03:2021 capec:CAPEC-66 attack:T1190'));
|
|
250
|
+
console.log('');
|
|
251
|
+
console.log(D(' Example:'));
|
|
252
|
+
console.log(EX(' // @exposes api.auth to SQL Injection [high] cwe:CWE-89 owasp:A03:2021'));
|
|
253
|
+
console.log('');
|
|
233
254
|
// ── TIPS ──────────────────────────────────────────────────────────
|
|
234
255
|
console.log(H(' ── Tips ────────────────────────────────────────────────────'));
|
|
235
256
|
console.log('');
|
|
257
|
+
console.log(D(' • Descriptions use -- "quoted text" format (not : colon)'));
|
|
258
|
+
console.log(D(' • Severity uses brackets: [critical] [high] [medium] [low] or [P0]-[P3]'));
|
|
236
259
|
console.log(D(' • Annotations work in any comment style: // /* # -- <!-- -->'));
|
|
237
260
|
console.log(D(' • Place annotations on the line ABOVE the code they describe'));
|
|
238
261
|
console.log(D(' • Asset names are case-insensitive and normalized (spaces→underscores)'));
|
|
239
262
|
console.log(D(' • Threat/control names can reference IDs with #id syntax'));
|
|
263
|
+
console.log(D(' • @flows uses -> arrow syntax (not "to")'));
|
|
240
264
|
console.log(D(' • Run /parse after adding annotations to update the threat model'));
|
|
241
265
|
console.log(D(' • Run /validate to check for syntax errors and dangling references'));
|
|
242
266
|
console.log(D(' • Run /annotate to have an AI agent add annotations automatically'));
|
|
@@ -1595,4 +1619,238 @@ export async function cmdDashboard(ctx) {
|
|
|
1595
1619
|
}
|
|
1596
1620
|
console.log('');
|
|
1597
1621
|
}
|
|
1622
|
+
// ─── /workspace ──────────────────────────────────────────────────────
|
|
1623
|
+
export function cmdWorkspace(ctx) {
|
|
1624
|
+
const config = loadWorkspaceConfig(ctx.root);
|
|
1625
|
+
if (!config) {
|
|
1626
|
+
console.log('');
|
|
1627
|
+
console.log(C.warn(' This repo is not part of a workspace.'));
|
|
1628
|
+
console.log(C.dim(' Use /link to create one, or guardlink link-project in the CLI.'));
|
|
1629
|
+
console.log('');
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
console.log('');
|
|
1633
|
+
console.log(` ${C.bold('Workspace:')} ${config.workspace}`);
|
|
1634
|
+
console.log(` ${C.bold('This repo:')} ${config.this_repo}`);
|
|
1635
|
+
console.log('');
|
|
1636
|
+
console.log(` ${C.bold('Linked repos')} (${config.repos.length}):`);
|
|
1637
|
+
for (const r of config.repos) {
|
|
1638
|
+
const isSelf = r.name === config.this_repo ? C.dim(' (this)') : '';
|
|
1639
|
+
const reg = r.registry ? C.dim(` → ${r.registry}`) : '';
|
|
1640
|
+
console.log(` ${r.name === config.this_repo ? C.green('●') : C.cyan('○')} ${r.name}${isSelf}${reg}`);
|
|
1641
|
+
}
|
|
1642
|
+
console.log('');
|
|
1643
|
+
console.log(C.dim(' /merge to combine reports · /link --add to add a repo · /link --remove to remove'));
|
|
1644
|
+
console.log('');
|
|
1645
|
+
}
|
|
1646
|
+
// ─── /link ───────────────────────────────────────────────────────────
|
|
1647
|
+
export async function cmdLink(args, ctx) {
|
|
1648
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
1649
|
+
// Parse flags
|
|
1650
|
+
let addPath;
|
|
1651
|
+
let removeName;
|
|
1652
|
+
let workspace = 'workspace';
|
|
1653
|
+
let registry;
|
|
1654
|
+
const repoPaths = [];
|
|
1655
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1656
|
+
const p = parts[i];
|
|
1657
|
+
if (p === '--add' && parts[i + 1]) {
|
|
1658
|
+
addPath = parts[++i];
|
|
1659
|
+
}
|
|
1660
|
+
else if (p === '--remove' && parts[i + 1]) {
|
|
1661
|
+
removeName = parts[++i];
|
|
1662
|
+
}
|
|
1663
|
+
else if ((p === '-w' || p === '--workspace') && parts[i + 1]) {
|
|
1664
|
+
workspace = parts[++i];
|
|
1665
|
+
}
|
|
1666
|
+
else if ((p === '-r' || p === '--registry') && parts[i + 1]) {
|
|
1667
|
+
registry = parts[++i];
|
|
1668
|
+
}
|
|
1669
|
+
else {
|
|
1670
|
+
repoPaths.push(p);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
if (removeName) {
|
|
1674
|
+
// ── Remove mode ──
|
|
1675
|
+
console.log(C.dim(` Removing "${removeName}" from workspace...`));
|
|
1676
|
+
const result = removeFromWorkspace({
|
|
1677
|
+
repoName: removeName,
|
|
1678
|
+
existingRepoPath: ctx.root,
|
|
1679
|
+
});
|
|
1680
|
+
for (const name of result.updated)
|
|
1681
|
+
console.log(` ${C.green('↻')} ${name} — updated`);
|
|
1682
|
+
for (const f of result.agentFilesUpdated) {
|
|
1683
|
+
if (f.includes('(cleaned)'))
|
|
1684
|
+
console.log(` ${C.dim('🧹')} ${f}`);
|
|
1685
|
+
}
|
|
1686
|
+
for (const s of result.skipped)
|
|
1687
|
+
console.log(` ${C.warn('✗')} ${s.name} — ${s.reason}`);
|
|
1688
|
+
if (result.updated.length > 0) {
|
|
1689
|
+
console.log('');
|
|
1690
|
+
console.log(C.success(` ✓ Removed "${removeName}", updated ${result.updated.length} repo(s)`));
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
else if (addPath) {
|
|
1694
|
+
// ── Add mode (--from is implicit: ctx.root) ──
|
|
1695
|
+
console.log(C.dim(` Adding ${addPath} to workspace...`));
|
|
1696
|
+
const result = addToWorkspace({
|
|
1697
|
+
newRepoPath: resolve(addPath),
|
|
1698
|
+
existingRepoPath: ctx.root,
|
|
1699
|
+
registry,
|
|
1700
|
+
});
|
|
1701
|
+
for (const name of result.initialized)
|
|
1702
|
+
console.log(` ${C.cyan('⚡')} ${name} — auto-initialized`);
|
|
1703
|
+
for (const name of result.linked)
|
|
1704
|
+
console.log(` ${C.green('✓')} ${name} — linked`);
|
|
1705
|
+
for (const name of result.updated)
|
|
1706
|
+
console.log(` ${C.green('↻')} ${name} — updated`);
|
|
1707
|
+
for (const s of result.skipped)
|
|
1708
|
+
console.log(` ${C.warn('✗')} ${s.name} — ${s.reason}`);
|
|
1709
|
+
if (result.linked.length > 0 || result.updated.length > 0) {
|
|
1710
|
+
console.log('');
|
|
1711
|
+
console.log(C.success(` ✓ ${result.linked.length} added, ${result.updated.length} updated`));
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
else if (repoPaths.length >= 2) {
|
|
1715
|
+
// ── Fresh link mode ──
|
|
1716
|
+
console.log(C.dim(` Linking ${repoPaths.length} repos into "${workspace}"...`));
|
|
1717
|
+
const result = linkProject({
|
|
1718
|
+
workspace,
|
|
1719
|
+
repoPaths: repoPaths.map(p => resolve(p)),
|
|
1720
|
+
registry,
|
|
1721
|
+
});
|
|
1722
|
+
for (const name of result.initialized)
|
|
1723
|
+
console.log(` ${C.cyan('⚡')} ${name} — auto-initialized`);
|
|
1724
|
+
for (const name of result.linked)
|
|
1725
|
+
console.log(` ${C.green('✓')} ${name} — linked`);
|
|
1726
|
+
for (const s of result.skipped)
|
|
1727
|
+
console.log(` ${C.warn('✗')} ${s.name} — ${s.reason}`);
|
|
1728
|
+
if (result.linked.length > 0) {
|
|
1729
|
+
console.log('');
|
|
1730
|
+
console.log(C.success(` ✓ Linked ${result.linked.length} repo(s) into "${workspace}"`));
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
else {
|
|
1734
|
+
console.log('');
|
|
1735
|
+
console.log(` ${C.bold('Usage:')}`);
|
|
1736
|
+
console.log(` /link <repo1> <repo2> ... ${C.dim('Fresh workspace setup')}`);
|
|
1737
|
+
console.log(` /link --add <repo-path> ${C.dim('Add a repo (uses current repo as reference)')}`);
|
|
1738
|
+
console.log(` /link --remove <repo-name> ${C.dim('Remove a repo by name')}`);
|
|
1739
|
+
console.log(` /link -w <name> -r <registry> ... ${C.dim('Set workspace name and registry')}`);
|
|
1740
|
+
}
|
|
1741
|
+
console.log('');
|
|
1742
|
+
}
|
|
1743
|
+
// ─── /merge ──────────────────────────────────────────────────────────
|
|
1744
|
+
export async function cmdMerge(args, ctx) {
|
|
1745
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
1746
|
+
// Parse flags
|
|
1747
|
+
let outputFile;
|
|
1748
|
+
let jsonFile;
|
|
1749
|
+
let diffAgainst;
|
|
1750
|
+
let workspaceName;
|
|
1751
|
+
const files = [];
|
|
1752
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1753
|
+
const p = parts[i];
|
|
1754
|
+
if ((p === '-o' || p === '--output') && parts[i + 1]) {
|
|
1755
|
+
outputFile = parts[++i];
|
|
1756
|
+
}
|
|
1757
|
+
else if (p === '--json' && parts[i + 1]) {
|
|
1758
|
+
jsonFile = parts[++i];
|
|
1759
|
+
}
|
|
1760
|
+
else if (p === '--diff-against' && parts[i + 1]) {
|
|
1761
|
+
diffAgainst = parts[++i];
|
|
1762
|
+
}
|
|
1763
|
+
else if ((p === '-w' || p === '--workspace') && parts[i + 1]) {
|
|
1764
|
+
workspaceName = parts[++i];
|
|
1765
|
+
}
|
|
1766
|
+
else {
|
|
1767
|
+
files.push(resolve(p));
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (files.length === 0) {
|
|
1771
|
+
console.log('');
|
|
1772
|
+
console.log(` ${C.bold('Usage:')}`);
|
|
1773
|
+
console.log(` /merge <report1.json> <report2.json> ...`);
|
|
1774
|
+
console.log(` /merge *.json -o dashboard.html --json merged.json`);
|
|
1775
|
+
console.log(` /merge *.json --diff-against last-week.json`);
|
|
1776
|
+
console.log('');
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
console.log(C.dim(` Merging ${files.length} report(s)...`));
|
|
1780
|
+
// Load and merge
|
|
1781
|
+
const reportJsons = [];
|
|
1782
|
+
for (const f of files) {
|
|
1783
|
+
if (!existsSync(f)) {
|
|
1784
|
+
console.log(C.warn(` ✗ File not found: ${f}`));
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
try {
|
|
1788
|
+
reportJsons.push(JSON.parse(readFileSync(f, 'utf-8')));
|
|
1789
|
+
}
|
|
1790
|
+
catch (err) {
|
|
1791
|
+
console.log(C.warn(` ✗ Invalid JSON: ${f}`));
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
if (reportJsons.length === 0) {
|
|
1795
|
+
console.log(C.error(' No valid reports to merge.'));
|
|
1796
|
+
console.log('');
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
const merged = await mergeReports(reportJsons, { workspace: workspaceName });
|
|
1800
|
+
// Summary
|
|
1801
|
+
const t = merged.totals;
|
|
1802
|
+
console.log('');
|
|
1803
|
+
console.log(` ${C.bold(merged.workspace)} — ${merged.repo_statuses.filter(r => r.loaded).length}/${merged.repo_statuses.length} repos loaded`);
|
|
1804
|
+
console.log(` ${t.annotations} annotations | ${t.assets} assets | ${t.threats} threats | ${t.controls} controls`);
|
|
1805
|
+
console.log(` ${t.mitigations} mitigations | ${t.exposures} exposures | ${t.unmitigated_exposures} unmitigated`);
|
|
1806
|
+
console.log(` ${t.flows} flows | ${t.external_refs_resolved} refs resolved | ${t.external_refs_unresolved} unresolved`);
|
|
1807
|
+
// Warnings
|
|
1808
|
+
for (const w of merged.warnings) {
|
|
1809
|
+
console.log(` ${C.warn('⚠')} ${w.message}`);
|
|
1810
|
+
}
|
|
1811
|
+
// Write JSON
|
|
1812
|
+
if (jsonFile) {
|
|
1813
|
+
writeFileSync(resolve(jsonFile), JSON.stringify(merged, null, 2));
|
|
1814
|
+
console.log(` ${C.green('✓')} Wrote merged JSON to ${jsonFile}`);
|
|
1815
|
+
}
|
|
1816
|
+
// Diff
|
|
1817
|
+
if (diffAgainst && existsSync(resolve(diffAgainst))) {
|
|
1818
|
+
try {
|
|
1819
|
+
const previous = JSON.parse(readFileSync(resolve(diffAgainst), 'utf-8'));
|
|
1820
|
+
const diff = diffMergedReports(merged, previous);
|
|
1821
|
+
if (diff.risk_delta === 'decreased') {
|
|
1822
|
+
console.log(` ${C.green('🟢')} Risk decreased since last merge`);
|
|
1823
|
+
}
|
|
1824
|
+
else if (diff.risk_delta === 'increased') {
|
|
1825
|
+
console.log(` ${C.red('🔴')} Risk increased since last merge`);
|
|
1826
|
+
}
|
|
1827
|
+
else {
|
|
1828
|
+
console.log(` ${C.dim('⚪')} Risk unchanged`);
|
|
1829
|
+
}
|
|
1830
|
+
if (diff.resolved_unmitigated > 0) {
|
|
1831
|
+
console.log(` ${C.green('🟢')} ${diff.resolved_unmitigated} exposure(s) now mitigated`);
|
|
1832
|
+
}
|
|
1833
|
+
if (diff.new_unmitigated > 0) {
|
|
1834
|
+
console.log(` ${C.red('🔴')} ${diff.new_unmitigated} new unmitigated exposure(s)`);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
catch {
|
|
1838
|
+
console.log(C.warn(` ✗ Could not parse diff file: ${diffAgainst}`));
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
// Dashboard
|
|
1842
|
+
if (outputFile || !jsonFile) {
|
|
1843
|
+
const dashPath = resolve(outputFile || 'workspace-dashboard.html');
|
|
1844
|
+
const html = generateDashboardHTML(merged.model);
|
|
1845
|
+
writeFileSync(dashPath, html);
|
|
1846
|
+
console.log(` ${C.green('✓')} Dashboard: ${outputFile || 'workspace-dashboard.html'}`);
|
|
1847
|
+
}
|
|
1848
|
+
// Print summary
|
|
1849
|
+
const summary = formatMergeSummary(merged);
|
|
1850
|
+
console.log('');
|
|
1851
|
+
for (const line of summary.split('\n').slice(0, 15)) {
|
|
1852
|
+
console.log(` ${line}`);
|
|
1853
|
+
}
|
|
1854
|
+
console.log('');
|
|
1855
|
+
}
|
|
1598
1856
|
//# sourceMappingURL=commands.js.map
|