codehere 0.7.1 → 1.0.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/README.md +22 -13
- package/dist/application/services/verdict-service.js +154 -0
- package/dist/application/use-cases/assess-cross-vendor.js +126 -0
- package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
- package/dist/application/use-cases/export-aat.js +107 -0
- package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
- package/dist/application/use-cases/generate-final-report.js +140 -15
- package/dist/application/use-cases/generate-narration.js +5 -3
- package/dist/application/use-cases/generate-thread-title.js +5 -2
- package/dist/application/use-cases/narrator-prompt-template.js +1 -1
- package/dist/application/use-cases/plan-auto-review.js +57 -0
- package/dist/application/use-cases/reconcile-audit.js +100 -0
- package/dist/application/use-cases/rerun-tests.js +226 -0
- package/dist/application/use-cases/search-sessions.js +70 -0
- package/dist/application/use-cases/should-auto-review.js +71 -0
- package/dist/application/use-cases/thread-export.js +90 -0
- package/dist/application/use-cases/verify-claim.js +187 -5
- package/dist/domain/audit.js +11 -0
- package/dist/domain/services/codebase-detector.js +0 -6
- package/dist/domain/task.js +91 -0
- package/dist/error-handler.js +3 -115
- package/dist/index.js +279 -31
- package/dist/infrastructure/completion/bash-completion.js +7 -3
- package/dist/infrastructure/completion/fish-completion.js +7 -12
- package/dist/infrastructure/completion/zsh-completion.js +7 -12
- package/dist/infrastructure/config/config-manager.js +4 -3
- package/dist/infrastructure/config/product-context.js +1 -19
- package/dist/infrastructure/context/adapters/figma-source.js +37 -2
- package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
- package/dist/infrastructure/context/github-api.js +110 -0
- package/dist/infrastructure/context/github-device-flow.js +65 -0
- package/dist/infrastructure/context/oauth-store.js +5 -2
- package/dist/infrastructure/context/registry-bootstrap.js +0 -4
- package/dist/infrastructure/cost/budget-config.js +4 -3
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +91 -16
- package/dist/infrastructure/cost/team-ledger.js +355 -0
- package/dist/infrastructure/discovery/discovery-log.js +85 -0
- package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
- package/dist/infrastructure/hooks/hook-mode.js +20 -4
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
- package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
- package/dist/infrastructure/observability/execution-tracer.js +1 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
- package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
- package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
- package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
- package/dist/infrastructure/orchestration/context-handoff.js +32 -14
- package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
- package/dist/infrastructure/orchestration/failover-config.js +1 -11
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/memory-injector.js +4 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
- package/dist/infrastructure/orchestration/session-manager.js +593 -94
- package/dist/infrastructure/orchestration/session-persistence.js +177 -41
- package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
- package/dist/infrastructure/protocols/cloud-sync.js +2 -2
- package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
- package/dist/infrastructure/protocols/mcp-tools.js +401 -0
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
- package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
- package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
- package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
- package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
- package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
- package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
- package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
- package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
- package/dist/infrastructure/protocols/ui/styles.js +2147 -0
- package/dist/infrastructure/protocols/ui/tokens.js +96 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
- package/dist/infrastructure/protocols/verify-page.js +884 -148
- package/dist/infrastructure/security/fix-code.js +144 -0
- package/dist/infrastructure/security/fix-secret.js +89 -0
- package/dist/infrastructure/security/permissions-config.js +3 -2
- package/dist/infrastructure/security/scan-types.js +10 -0
- package/dist/infrastructure/security/ship-scan.js +195 -0
- package/dist/infrastructure/security/trust-scorer.js +70 -3
- package/dist/infrastructure/security/vibe-checks.js +99 -0
- package/dist/infrastructure/storage/atomic-write.js +86 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
- package/dist/infrastructure/storage/project-thread-reader.js +262 -25
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/monitoring.js +2 -2
- package/dist/presentation/cli/commands/audit-command.js +79 -4
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/check-command.js +96 -0
- package/dist/presentation/cli/commands/context-source-command.js +20 -1
- package/dist/presentation/cli/commands/help-command.js +5 -5
- package/dist/presentation/cli/commands/hook-command.js +5 -5
- package/dist/presentation/cli/commands/init-command.js +89 -66
- package/dist/presentation/cli/commands/memory-command.js +28 -28
- package/dist/presentation/cli/commands/permission-command.js +1 -12
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/protocols-command.js +4 -4
- package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
- package/dist/presentation/cli/commands/report-command.js +48 -32
- package/dist/presentation/cli/commands/scan-command.js +52 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
- package/dist/presentation/cli/commands/search-command.js +51 -0
- package/dist/presentation/cli/commands/setup-command.js +31 -8
- package/dist/presentation/cli/commands/status-command.js +39 -86
- package/dist/presentation/cli/commands/team-command.js +256 -0
- package/dist/presentation/cli/commands/thread-command.js +87 -5
- package/dist/presentation/cli/commands/trust-command.js +53 -31
- package/dist/presentation/cli/commands/verify-command.js +74 -9
- package/dist/presentation/cli/error-display.js +0 -8
- package/dist/presentation/cli/repl.js +15 -7
- package/dist/presentation/cli/task-deliverable.js +36 -10
- package/dist/testing/replay-adapter.js +0 -19
- package/dist/ui.js +51 -239
- package/dist/utils/version.js +0 -6
- package/package.json +30 -10
- package/dist/domain/entities/task.js +0 -61
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/task-repository.js +0 -206
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `codehere search "<query>"` — REQUEST_METHOD stage 7 (Learn) / #19.
|
|
3
|
+
* Search past agent sessions with PROVENANCE: which agent ran it, whether it
|
|
4
|
+
* succeeded, in which project. Local-first (reads on-disk records, no network).
|
|
5
|
+
* Follows the CLI color economy (DESIGN.md §5.2): colour carries meaning, and
|
|
6
|
+
* the output stays legible under NO_COLOR (glyph + word carry the status).
|
|
7
|
+
*/
|
|
8
|
+
import { searchSessions } from '../../../application/use-cases/search-sessions.js';
|
|
9
|
+
import { colors, symbols, newline } from '../../../ui.js';
|
|
10
|
+
function statusGlyph(status) {
|
|
11
|
+
const s = status.toLowerCase();
|
|
12
|
+
if (/completed|done/.test(s))
|
|
13
|
+
return colors.verified(symbols.success); // ✓ verified
|
|
14
|
+
if (/error|caught|stopped|failed/.test(s))
|
|
15
|
+
return colors.caught(symbols.error); // ✕
|
|
16
|
+
return colors.dim('·'); // · pending / other
|
|
17
|
+
}
|
|
18
|
+
export function executeSearchCommand(query, options) {
|
|
19
|
+
const limit = options.limit ? Math.max(1, parseInt(options.limit, 10) || 10) : 10;
|
|
20
|
+
const hits = searchSessions(query, { limit });
|
|
21
|
+
if (options.json) {
|
|
22
|
+
process.stdout.write(JSON.stringify(hits, null, 2) + '\n');
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
if (!hits.length) {
|
|
26
|
+
process.stdout.write(colors.dim(`No past sessions match "${query}".\n`));
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
newline();
|
|
30
|
+
process.stdout.write(colors.heading(`${hits.length} past session${hits.length === 1 ? '' : 's'} for "${query}"`) +
|
|
31
|
+
colors.dim(' (provenance: which agent · whether it succeeded)') +
|
|
32
|
+
'\n\n');
|
|
33
|
+
for (const h of hits) {
|
|
34
|
+
// provenance line: agent · verdict · status · id · project
|
|
35
|
+
process.stdout.write(' ' +
|
|
36
|
+
colors.identity(h.agent.padEnd(12)) +
|
|
37
|
+
' ' +
|
|
38
|
+
statusGlyph(h.status) +
|
|
39
|
+
' ' +
|
|
40
|
+
colors.dim(h.status.padEnd(10)) +
|
|
41
|
+
' ' +
|
|
42
|
+
colors.evidence(h.sessionId.slice(0, 8)) +
|
|
43
|
+
' ' +
|
|
44
|
+
colors.dim(h.project) +
|
|
45
|
+
'\n');
|
|
46
|
+
process.stdout.write(' ' + colors.dim(h.snippet) + '\n\n');
|
|
47
|
+
}
|
|
48
|
+
process.stdout.write(colors.dim('Open one: ') + colors.bold('codehere verify <id>') + '\n\n');
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=search-command.js.map
|
|
@@ -39,7 +39,11 @@ function printAgentLines(all) {
|
|
|
39
39
|
console.log(warning(`${a.label}: installed, needs sign-in`) + hint);
|
|
40
40
|
}
|
|
41
41
|
else if (a.state === 'billed_key_required') {
|
|
42
|
-
|
|
42
|
+
// Surface the adapter's own authHint instead of a hardcoded "paid" label —
|
|
43
|
+
// some agents (Aider via LiteLLM) actually have a $0 path the detail names.
|
|
44
|
+
console.log(colors.dim(`○ ${a.label}: installed, needs a model + key`));
|
|
45
|
+
if (a.authHint?.detail)
|
|
46
|
+
console.log(colors.dim(` ${a.authHint.detail}`));
|
|
43
47
|
}
|
|
44
48
|
else {
|
|
45
49
|
const hint = a.installHint?.command ? colors.dim(` (install: ${a.installHint.command})`) : '';
|
|
@@ -47,6 +51,27 @@ function printAgentLines(all) {
|
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The free-tier setup recipe (Phase 1 — free-tier-first). One compact,
|
|
56
|
+
* copy-pasteable line per vendor showing the CHEAPEST way to get it running —
|
|
57
|
+
* so a user testing all 5 vendors knows exactly how to do it for ~$0. Shown
|
|
58
|
+
* when no agent is ready yet (the moment the user needs it). Mirrors the
|
|
59
|
+
* per-agent readiness authHints; kept here as one consolidated cheatsheet
|
|
60
|
+
* because the readiness hints surface one-at-a-time, not as a plan.
|
|
61
|
+
*/
|
|
62
|
+
function printFreeTierRecipe() {
|
|
63
|
+
console.log(colors.bold('Set up any agent for free (or near-free):'));
|
|
64
|
+
console.log(colors.cyan(' Claude') +
|
|
65
|
+
colors.dim(' run `claude` › /login · uses your Claude subscription (no API key, no per-token bill)'));
|
|
66
|
+
console.log(colors.cyan(' Gemini') +
|
|
67
|
+
colors.dim(' export GEMINI_API_KEY=… free tier › https://aistudio.google.com/apikey'));
|
|
68
|
+
console.log(colors.cyan(' Codex') +
|
|
69
|
+
colors.dim(' run `codex login` with a ChatGPT Plus/Pro account · flat, no per-token charge'));
|
|
70
|
+
console.log(colors.cyan(' OpenCode') +
|
|
71
|
+
colors.dim(' run `opencode auth` · point at a local Ollama model or a free provider ($0)'));
|
|
72
|
+
console.log(colors.cyan(' Aider') +
|
|
73
|
+
colors.dim(' reuse the free Gemini key: `--model gemini/gemini-2.5-flash` (routes via LiteLLM, $0)'));
|
|
74
|
+
}
|
|
50
75
|
/**
|
|
51
76
|
* Check if any API key is configured
|
|
52
77
|
*/
|
|
@@ -162,9 +187,9 @@ export async function executeSetupCommand(force = false) {
|
|
|
162
187
|
console.log(colors.dim(' Next: codehere start (open the dashboard)'));
|
|
163
188
|
}
|
|
164
189
|
else {
|
|
165
|
-
console.log(warning('No coding agent is set up yet.
|
|
166
|
-
console.log(
|
|
167
|
-
|
|
190
|
+
console.log(warning('No coding agent is set up yet.'));
|
|
191
|
+
console.log();
|
|
192
|
+
printFreeTierRecipe();
|
|
168
193
|
}
|
|
169
194
|
return;
|
|
170
195
|
}
|
|
@@ -206,15 +231,13 @@ export async function executeSetupCommand(force = false) {
|
|
|
206
231
|
else if (apiKeyCheck.configured) {
|
|
207
232
|
// No agent CLI ready, but a key is present — some adapters (e.g. Aider) run off it.
|
|
208
233
|
console.log(success(`${apiKeyCheck.provider} API key found.`));
|
|
209
|
-
console.log(colors.dim(' No agent CLI detected yet
|
|
234
|
+
console.log(colors.dim(' No agent CLI detected yet · install one for the full experience.'));
|
|
210
235
|
result.apiKeyConfigured = true;
|
|
211
236
|
}
|
|
212
237
|
else {
|
|
213
238
|
console.log(warning('No coding agent is set up yet.'));
|
|
214
239
|
newline();
|
|
215
|
-
|
|
216
|
-
console.log(colors.cyan(' npm i -g @anthropic-ai/claude-code') + colors.dim(' then run: claude (sign in)'));
|
|
217
|
-
console.log(colors.dim(' Already use one by API key? export ANTHROPIC_API_KEY=sk-ant-...'));
|
|
240
|
+
printFreeTierRecipe();
|
|
218
241
|
newline();
|
|
219
242
|
}
|
|
220
243
|
newline();
|
|
@@ -17,13 +17,17 @@ import { getVersion } from '../../../utils/version.js';
|
|
|
17
17
|
import { loadProductContext } from '../../../infrastructure/config/product-context.js';
|
|
18
18
|
import { AUDIT_FILE } from '../../../infrastructure/hooks/claude-sdk-hook.js';
|
|
19
19
|
import { analyzeSessions, detectAmplificationAlerts, formatDuration, } from '../../../infrastructure/hooks/session-analyzer.js';
|
|
20
|
-
|
|
20
|
+
// Reads the FULL audit log. A prior 5000-line cap fed the "All time" column —
|
|
21
|
+
// so with >5000 entries the dashboard showed e.g. "All time: 5000" when the
|
|
22
|
+
// true total was ~8k (DISCOVERED-006: a precise number that's wrong is worse
|
|
23
|
+
// than none). The recent-blocks list caps itself downstream via slice(-20);
|
|
24
|
+
// the time-window stats filter by timestamp — neither needs a read cap.
|
|
25
|
+
function readAuditLog() {
|
|
21
26
|
if (!existsSync(AUDIT_FILE))
|
|
22
27
|
return [];
|
|
23
28
|
try {
|
|
24
29
|
const lines = readFileSync(AUDIT_FILE, 'utf-8').split('\n').filter(Boolean);
|
|
25
|
-
|
|
26
|
-
return recent
|
|
30
|
+
return lines
|
|
27
31
|
.map((l) => {
|
|
28
32
|
try {
|
|
29
33
|
return JSON.parse(l);
|
|
@@ -41,6 +45,24 @@ function readAuditLog(maxLines = 5000) {
|
|
|
41
45
|
function withinHours(timestamp, hours) {
|
|
42
46
|
return Date.now() - new Date(timestamp).getTime() < hours * 3600 * 1000;
|
|
43
47
|
}
|
|
48
|
+
// Real daemon reachability — the "Status" line was a hardcoded "Running" that
|
|
49
|
+
// stayed green even with no daemon up (a green-on-failure lie at the surface).
|
|
50
|
+
// Probe /health with a short timeout so the dashboard reports the true state.
|
|
51
|
+
async function probeDaemonRunning(port = parseInt(process.env.CODEHERE_PORT ?? '3773', 10) || 3773, host = '127.0.0.1') {
|
|
52
|
+
const http = await import('http');
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const req = http.get({ host, port, path: '/health', timeout: 500 }, (res) => {
|
|
55
|
+
const ok = res.statusCode === 200;
|
|
56
|
+
res.resume();
|
|
57
|
+
resolve(ok);
|
|
58
|
+
});
|
|
59
|
+
req.on('error', () => resolve(false));
|
|
60
|
+
req.on('timeout', () => {
|
|
61
|
+
req.destroy();
|
|
62
|
+
resolve(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
44
66
|
function timeAgo(timestamp) {
|
|
45
67
|
const ms = Date.now() - new Date(timestamp).getTime();
|
|
46
68
|
const mins = Math.floor(ms / 60000);
|
|
@@ -126,7 +148,7 @@ function renderSessionDrillDown(sessionId, entries) {
|
|
|
126
148
|
const tool = colors.cyan(entry.toolName.padEnd(12));
|
|
127
149
|
const reason = extractBlockReason(entry);
|
|
128
150
|
const risk = entry.riskScore > 70 ? colors.red(`risk:${entry.riskScore}`) : colors.yellow(`risk:${entry.riskScore}`);
|
|
129
|
-
console.log(` ${colors.red('
|
|
151
|
+
console.log(` ${colors.red('✕')} ${time} ${tool} ${risk} ${colors.dim(reason)}`);
|
|
130
152
|
}
|
|
131
153
|
newline();
|
|
132
154
|
}
|
|
@@ -143,7 +165,7 @@ function renderSessionDrillDown(sessionId, entries) {
|
|
|
143
165
|
for (const entry of toShow) {
|
|
144
166
|
const time = colors.dim(new Date(entry.timestamp).toLocaleTimeString());
|
|
145
167
|
const tool = (entry.blocked ? colors.red : colors.cyan)(entry.toolName.padEnd(12));
|
|
146
|
-
const icon = entry.blocked ? colors.red('
|
|
168
|
+
const icon = entry.blocked ? colors.red('✕') : entry.criticalFindings > 0 ? colors.yellow('!') : colors.green('✓');
|
|
147
169
|
const reason = extractBlockReason(entry);
|
|
148
170
|
console.log(` ${icon} ${time} ${tool} ${colors.dim(reason)}`);
|
|
149
171
|
}
|
|
@@ -152,16 +174,16 @@ function renderSessionDrillDown(sessionId, entries) {
|
|
|
152
174
|
// (UX_OBSERVATIONS.md P1; long attended sessions shouldn't get accused).
|
|
153
175
|
if (blocked.length > 2 && sorted.length > 20 && blocked.length / sorted.length > 0.05) {
|
|
154
176
|
const pct = ((blocked.length / sorted.length) * 100).toFixed(0);
|
|
155
|
-
console.log(colors.red(`
|
|
177
|
+
console.log(colors.red(` ! ${pct}% block rate (${blocked.length}/${sorted.length}) · review for injection patterns.`));
|
|
156
178
|
}
|
|
157
179
|
else if (blocked.length > 0 && sorted.length > 20) {
|
|
158
|
-
console.log(colors.yellow(` ${blocked.length} blocked / ${sorted.length} total
|
|
180
|
+
console.log(colors.yellow(` ${blocked.length} blocked / ${sorted.length} total · long session, review if unattended.`));
|
|
159
181
|
}
|
|
160
182
|
else if (blocked.length > 0) {
|
|
161
|
-
console.log(colors.yellow(` ${blocked.length} blocked call${blocked.length !== 1 ? 's' : ''}
|
|
183
|
+
console.log(colors.yellow(` ${blocked.length} blocked call${blocked.length !== 1 ? 's' : ''} · review the blocked entries above.`));
|
|
162
184
|
}
|
|
163
185
|
else if (sorted.length > 50) {
|
|
164
|
-
console.log(colors.yellow(` ${sorted.length} calls in one session
|
|
186
|
+
console.log(colors.yellow(` ${sorted.length} calls in one session · check for amplification.`));
|
|
165
187
|
}
|
|
166
188
|
else {
|
|
167
189
|
console.log(colors.green(' Session looks clean.'));
|
|
@@ -403,7 +425,7 @@ function exportAudit(entries, format, sinceHours) {
|
|
|
403
425
|
console.log('|---|---|---|---:|:---:|---:|');
|
|
404
426
|
for (const e of filtered) {
|
|
405
427
|
const sid = (e.sessionId || '').substring(0, 8);
|
|
406
|
-
const blocked = e.blocked ? '
|
|
428
|
+
const blocked = e.blocked ? '✕' : '✓';
|
|
407
429
|
console.log(`| \`${e.timestamp}\` | \`${sid}\` | ${e.toolName} | ${e.riskScore} | ${blocked} | ${e.findings} |`);
|
|
408
430
|
}
|
|
409
431
|
return;
|
|
@@ -456,11 +478,13 @@ export async function executeStatusCommand(options = {}) {
|
|
|
456
478
|
const productCtx = loadProductContext(cwd);
|
|
457
479
|
const hookRunning = existsSync(join(homedir(), '.claude', 'settings.json'));
|
|
458
480
|
const hasSkills = existsSync(join(homedir(), '.claude', 'commands', 'codehere'));
|
|
481
|
+
const daemonRunning = await probeDaemonRunning();
|
|
459
482
|
// ── JSON mode ──────────────────────────────────────────────────────────────
|
|
460
483
|
if (options.json) {
|
|
461
484
|
const result = {
|
|
462
485
|
version,
|
|
463
486
|
status: 'ok',
|
|
487
|
+
daemon: { running: daemonRunning },
|
|
464
488
|
hooks: {
|
|
465
489
|
running: hookRunning,
|
|
466
490
|
stats: { last24h: s24, last7d: s7d, allTime: sAll },
|
|
@@ -468,35 +492,6 @@ export async function executeStatusCommand(options = {}) {
|
|
|
468
492
|
skills: { installed: hasSkills },
|
|
469
493
|
product: productCtx,
|
|
470
494
|
};
|
|
471
|
-
if (options.tasks) {
|
|
472
|
-
try {
|
|
473
|
-
const { getTaskRepository } = await import('../../../infrastructure/storage/task-repository.js');
|
|
474
|
-
const taskRepo = getTaskRepository();
|
|
475
|
-
const tasks = await taskRepo.getAll();
|
|
476
|
-
result.tasks = tasks.slice(0, 20).map((t) => ({
|
|
477
|
-
id: t.id,
|
|
478
|
-
mode: t.mode,
|
|
479
|
-
status: t.status,
|
|
480
|
-
riskTier: t.riskTier,
|
|
481
|
-
environment: t.environment,
|
|
482
|
-
goal: t.goal,
|
|
483
|
-
createdAt: t.createdAt,
|
|
484
|
-
}));
|
|
485
|
-
result.taskCount = tasks.length;
|
|
486
|
-
}
|
|
487
|
-
catch (e) {
|
|
488
|
-
// rc.25: JSON consumers reading only stdout were still being fed a
|
|
489
|
-
// lie ([] tasks, 0 count) on real failure (Codex final review:
|
|
490
|
-
// "degraded behavior being presented as success"). Now we surface a
|
|
491
|
-
// structured taskLoadError + status:'degraded' so automation can
|
|
492
|
-
// distinguish "no tasks" from "load failed."
|
|
493
|
-
console.error('[status --tasks --json] task repository failed to load:', e);
|
|
494
|
-
result.tasks = [];
|
|
495
|
-
result.taskCount = 0;
|
|
496
|
-
result.taskLoadError = e instanceof Error ? e.message : String(e);
|
|
497
|
-
result.status = 'degraded';
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
495
|
console.log(JSON.stringify(result, null, 2));
|
|
501
496
|
return;
|
|
502
497
|
}
|
|
@@ -504,8 +499,9 @@ export async function executeStatusCommand(options = {}) {
|
|
|
504
499
|
sectionHeader('Codehere Status');
|
|
505
500
|
newline();
|
|
506
501
|
// Version + hooks
|
|
507
|
-
const hookStatus = hookRunning ? colors.green('✓') + ' Hooks running' : colors.yellow('
|
|
508
|
-
|
|
502
|
+
const hookStatus = hookRunning ? colors.green('✓') + ' Hooks running' : colors.yellow('!') + ' Hooks not installed';
|
|
503
|
+
const daemonStatus = daemonRunning ? colors.green('●') + ' Daemon running' : colors.dim('○') + ' Daemon stopped';
|
|
504
|
+
console.log(` Version: ${version} · ${daemonStatus} · ${hookStatus}`);
|
|
509
505
|
newline();
|
|
510
506
|
// Product brain
|
|
511
507
|
if (productCtx) {
|
|
@@ -557,13 +553,13 @@ export async function executeStatusCommand(options = {}) {
|
|
|
557
553
|
const tool = colors.cyan(entry.toolName.padEnd(10));
|
|
558
554
|
const time = colors.dim(timeAgo(entry.timestamp).padEnd(8));
|
|
559
555
|
const reason = extractBlockReason(entry);
|
|
560
|
-
console.log(` ${colors.red('
|
|
556
|
+
console.log(` ${colors.red('✕')} ${time} ${tool} ${colors.dim(reason)}`);
|
|
561
557
|
}
|
|
562
558
|
newline();
|
|
563
559
|
}
|
|
564
560
|
// If nothing was ever blocked, celebrate
|
|
565
561
|
if (sAll.blocked === 0) {
|
|
566
|
-
console.log(` ${colors.green('✓')} Nothing blocked yet
|
|
562
|
+
console.log(` ${colors.green('✓')} Nothing blocked yet · all scans clean.`);
|
|
567
563
|
newline();
|
|
568
564
|
}
|
|
569
565
|
}
|
|
@@ -609,49 +605,6 @@ export async function executeStatusCommand(options = {}) {
|
|
|
609
605
|
}
|
|
610
606
|
// Removed: Cost router jargon — not meaningful to users
|
|
611
607
|
// Tasks table (--tasks flag)
|
|
612
|
-
if (options.tasks) {
|
|
613
|
-
newline();
|
|
614
|
-
console.log(colors.bold('Recent tasks (experimental)'));
|
|
615
|
-
console.log(colors.dim('Task storage: .codehere/tasks.jsonl'));
|
|
616
|
-
newline();
|
|
617
|
-
try {
|
|
618
|
-
const { getTaskRepository } = await import('../../../infrastructure/storage/task-repository.js');
|
|
619
|
-
const taskRepo = getTaskRepository();
|
|
620
|
-
const tasks = await taskRepo.getAll();
|
|
621
|
-
if (tasks.length === 0) {
|
|
622
|
-
console.log(colors.dim('No tasks found. Run "codehere start" to open the task intake, or "codehere task <prompt>" from the CLI.'));
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
|
-
const pad = (s, n) => (s.length >= n ? s.substring(0, n) : s + ' '.repeat(n - s.length));
|
|
626
|
-
const trunc = (s, n) => (s.length <= n ? s : s.substring(0, n - 1) + '…');
|
|
627
|
-
console.log(colors.dim([pad('ID', 10), pad('Mode', 8), pad('Status', 12), pad('Risk', 4), pad('Env', 6), 'Goal'].join(' ')));
|
|
628
|
-
console.log(colors.dim(['-'.repeat(10), '-'.repeat(8), '-'.repeat(12), '-'.repeat(4), '-'.repeat(6), '-'.repeat(30)].join(' ')));
|
|
629
|
-
tasks.slice(0, 10).forEach((task) => {
|
|
630
|
-
console.log([
|
|
631
|
-
pad(task.id.substring(0, 8) + '..', 10),
|
|
632
|
-
pad(task.mode, 8),
|
|
633
|
-
pad(task.status, 12),
|
|
634
|
-
pad(task.riskTier.toUpperCase(), 4),
|
|
635
|
-
pad(task.environment.substring(0, 6), 6),
|
|
636
|
-
trunc(task.goal, 30),
|
|
637
|
-
].join(' '));
|
|
638
|
-
});
|
|
639
|
-
if (tasks.length > 10) {
|
|
640
|
-
newline();
|
|
641
|
-
console.log(colors.dim(`... and ${tasks.length - 10} more tasks`));
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
catch (e) {
|
|
646
|
-
// rc.25: don't print "No tasks found" — that's a lie when load failed
|
|
647
|
-
// (Codex final review: "degraded behavior presented as success"). Show
|
|
648
|
-
// a clear error line; stderr already has the cause for the operator.
|
|
649
|
-
console.error('[status] task repository failed to load:', e);
|
|
650
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
651
|
-
console.log(colors.red(` ✗ Could not load task repository: ${msg}`));
|
|
652
|
-
console.log(colors.dim(' See logs above for details. Try `codehere start` to (re)init the task intake.'));
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
608
|
// Quick trust scan (opt-in)
|
|
656
609
|
if (options.trust) {
|
|
657
610
|
const { executeTrustCommand } = await import('./trust-command.js');
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `codehere team`, the CLI face of the local-first team cost+audit ledger.
|
|
3
|
+
*
|
|
4
|
+
* The data layer (config, snapshots, merge, git sync) lives in
|
|
5
|
+
* infrastructure/cost/team-ledger.ts so the CLI and the daemon (`GET /api/team`)
|
|
6
|
+
* read the same core. This file is just the terminal rendering + subcommands.
|
|
7
|
+
*
|
|
8
|
+
* Subcommands:
|
|
9
|
+
* (none) / sync write this machine's snapshot, sync the team repo, show merged view
|
|
10
|
+
* init [path] create a team: make + git-init a shared folder and point at it
|
|
11
|
+
* join <git-url> join a team: clone the shared repo and point at it
|
|
12
|
+
* set-dir <path> point at an existing shared team directory
|
|
13
|
+
* budget <usd> set the team's shared daily ceiling (off = clear)
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
16
|
+
import { execFileSync } from 'child_process';
|
|
17
|
+
import { join as pathJoin, basename } from 'path';
|
|
18
|
+
import { loadTeamConfig, saveTeamConfig, codehereHome, initTeamDir, isGitRepo, gitPull, gitShareMine, writeMyShare, buildTeamView, } from '../../../infrastructure/cost/team-ledger.js';
|
|
19
|
+
import { success, error, info, heading, newline } from '../../../ui.js';
|
|
20
|
+
function fmtUsd(n) {
|
|
21
|
+
return n >= 0.01 || n === 0 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
|
|
22
|
+
}
|
|
23
|
+
export async function executeTeamCommand(subcommand, value, opts) {
|
|
24
|
+
const sub = subcommand ?? 'sync';
|
|
25
|
+
switch (sub) {
|
|
26
|
+
case 'sync':
|
|
27
|
+
case 'show':
|
|
28
|
+
return syncAndShow(Boolean(opts?.strict));
|
|
29
|
+
case 'init':
|
|
30
|
+
return initTeam(value);
|
|
31
|
+
case 'join':
|
|
32
|
+
if (!value) {
|
|
33
|
+
console.error(error('Usage:'), 'codehere team join <git-url>');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
return joinTeam(value);
|
|
37
|
+
case 'set-dir':
|
|
38
|
+
if (!value) {
|
|
39
|
+
console.error(error('Usage:'), 'codehere team set-dir <path-to-shared-dir>');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
return setDir(value);
|
|
43
|
+
case 'budget':
|
|
44
|
+
if (!value) {
|
|
45
|
+
console.error(error('Usage:'), 'codehere team budget <usd> (or `off`)');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
return setBudget(value);
|
|
49
|
+
default:
|
|
50
|
+
console.error(error('Unknown subcommand:'), sub);
|
|
51
|
+
console.error(info('Try:'), 'codehere team [sync | init [path] | join <git-url> | set-dir <path> | budget <usd>]');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function setDir(path) {
|
|
56
|
+
const cfg = loadTeamConfig();
|
|
57
|
+
cfg.dir = path;
|
|
58
|
+
saveTeamConfig(cfg);
|
|
59
|
+
console.log(success('Team directory set:'), path);
|
|
60
|
+
console.log(info('Next:'), 'run `codehere team` on each machine. Keep that folder in your own git/shared drive.');
|
|
61
|
+
}
|
|
62
|
+
// Create a team: make + git-init a shared folder, point at it, write the first
|
|
63
|
+
// snapshot — so a lead sets up a team in one command, no hand-editing config.
|
|
64
|
+
function initTeam(dir) {
|
|
65
|
+
const target = dir || pathJoin(codehereHome(), 'team-shared');
|
|
66
|
+
try {
|
|
67
|
+
initTeamDir(target); // shared core: mkdir + git-init + README (also used by POST /api/team/init)
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
console.error(error('Could not create team dir:'), target, '— check the path and permissions.');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
const cfg = loadTeamConfig();
|
|
74
|
+
cfg.dir = target;
|
|
75
|
+
saveTeamConfig(cfg);
|
|
76
|
+
try {
|
|
77
|
+
writeMyShare(target);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* best-effort first snapshot */
|
|
81
|
+
}
|
|
82
|
+
console.log(success('Team created:'), target);
|
|
83
|
+
console.log(info('Share it:'), 'push this folder to a git repo, then each teammate runs `codehere team join <git-url>`.');
|
|
84
|
+
}
|
|
85
|
+
// Join a team: clone the shared repo under ~/.codehere/teams/<name>, point at it,
|
|
86
|
+
// write this machine's first snapshot.
|
|
87
|
+
function joinTeam(url) {
|
|
88
|
+
// A join URL is shared/pasted, so it's a real attack surface. git's ext::/fd::
|
|
89
|
+
// (and file::) transports execute arbitrary commands, and a leading-dash URL is
|
|
90
|
+
// parsed as a git option (argument injection). Allow only real remote schemes.
|
|
91
|
+
if (/^-/.test(url) || /^(ext|fd|file)::/i.test(url) || !/^(https?:\/\/|git:\/\/|ssh:\/\/|git@[^/]+:)/.test(url)) {
|
|
92
|
+
console.error(error('Unsupported git URL:'), url);
|
|
93
|
+
console.error(info('Use:'), 'https:// , git:// , ssh:// , or git@host:owner/repo');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const name = basename(url.replace(/\.git$/, '')) || 'team-shared';
|
|
97
|
+
const target = pathJoin(codehereHome(), 'teams', name);
|
|
98
|
+
if (existsSync(target)) {
|
|
99
|
+
console.error(error('Already exists:'), target);
|
|
100
|
+
console.error(info('Point at it instead:'), `codehere team set-dir ${target}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
mkdirSync(pathJoin(codehereHome(), 'teams'), { recursive: true });
|
|
105
|
+
// Belt-and-suspenders: disable the command-executing transports at the git
|
|
106
|
+
// level too, and `--` so the URL can never be read as an option.
|
|
107
|
+
execFileSync('git', ['-c', 'protocol.ext.allow=never', '-c', 'protocol.allow=user', 'clone', '-q', '--', url, target], { stdio: 'inherit' });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
console.error(error('Clone failed:'), url, '— check the URL and your access.');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
if (!existsSync(pathJoin(target, 'spend')))
|
|
114
|
+
mkdirSync(pathJoin(target, 'spend'), { recursive: true });
|
|
115
|
+
const cfg = loadTeamConfig();
|
|
116
|
+
cfg.dir = target;
|
|
117
|
+
saveTeamConfig(cfg);
|
|
118
|
+
try {
|
|
119
|
+
writeMyShare(target);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
/* best-effort */
|
|
123
|
+
}
|
|
124
|
+
console.log(success('Joined team:'), target);
|
|
125
|
+
console.log(info('Next:'), 'run `codehere team` to sync and see the merged view.');
|
|
126
|
+
}
|
|
127
|
+
function setBudget(value) {
|
|
128
|
+
const cfg = loadTeamConfig();
|
|
129
|
+
if (value.toLowerCase() === 'off') {
|
|
130
|
+
cfg.dailyUsd = null;
|
|
131
|
+
saveTeamConfig(cfg);
|
|
132
|
+
console.log(success('Team daily budget cleared.'));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const num = Number(value.replace(/^\$/, ''));
|
|
136
|
+
if (!Number.isFinite(num) || num <= 0) {
|
|
137
|
+
console.error(error('Invalid amount:'), value, '(use a positive number, e.g. `codehere team budget 50`)');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
cfg.dailyUsd = num;
|
|
141
|
+
saveTeamConfig(cfg);
|
|
142
|
+
console.log(success('Team daily budget set:'), fmtUsd(num));
|
|
143
|
+
}
|
|
144
|
+
function syncAndShow(strict = false) {
|
|
145
|
+
const cfg = loadTeamConfig();
|
|
146
|
+
if (!cfg.dir) {
|
|
147
|
+
console.error(error('No team directory set.'));
|
|
148
|
+
console.error(info('Set one (a folder your team controls, e.g. a shared git repo):'));
|
|
149
|
+
console.error(' codehere team set-dir /path/to/team-shared-dir');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
if (!existsSync(cfg.dir)) {
|
|
153
|
+
console.error(error('Team directory does not exist:'), cfg.dir);
|
|
154
|
+
console.error(info('Create it (or clone the team repo) first, then re-run.'));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
const gitMode = isGitRepo(cfg.dir);
|
|
158
|
+
const pulled = gitMode ? gitPull(cfg.dir) : false;
|
|
159
|
+
const mine = writeMyShare(cfg.dir);
|
|
160
|
+
const pushed = gitMode ? gitShareMine(cfg.dir, `spend/${mine.machineId}.json`) : null;
|
|
161
|
+
const view = buildTeamView(cfg.dir, cfg.dailyUsd);
|
|
162
|
+
newline();
|
|
163
|
+
console.log(heading(`Team spend today (${view.date}), estimate, not a provider invoice`));
|
|
164
|
+
console.log(' ' + '─'.repeat(48));
|
|
165
|
+
for (const m of view.machines) {
|
|
166
|
+
const me = m.machineId === view.myMachineId ? ' (this machine)' : '';
|
|
167
|
+
console.log(` ${m.machineId.padEnd(28)} ${fmtUsd(m.totalUsd).padStart(9)}${me}`);
|
|
168
|
+
}
|
|
169
|
+
const agents = Object.entries(view.perAgent).sort((a, b) => b[1] - a[1]);
|
|
170
|
+
if (agents.length) {
|
|
171
|
+
console.log(' ' + '─'.repeat(48));
|
|
172
|
+
console.log(info(' by agent (cost · what each did)'));
|
|
173
|
+
for (const [agent, usd] of agents) {
|
|
174
|
+
const sess = view.perAgentSessions[agent] ?? 0;
|
|
175
|
+
console.log(` ${agent.padEnd(20)} ${fmtUsd(usd).padStart(9)} ${String(sess).padStart(3)} session${sess === 1 ? '' : 's'}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const projects = Object.entries(view.perProject).sort((a, b) => b[1] - a[1]);
|
|
179
|
+
if (projects.length) {
|
|
180
|
+
console.log(' ' + '─'.repeat(48));
|
|
181
|
+
console.log(info(' by project'));
|
|
182
|
+
for (const [proj, usd] of projects)
|
|
183
|
+
console.log(` ${proj.padEnd(28)} ${fmtUsd(usd).padStart(9)}`);
|
|
184
|
+
}
|
|
185
|
+
// Practice runs sit OUTSIDE the totals (their cost is the mock's own
|
|
186
|
+
// fabrication, never billed) — one quiet line, only when any exist.
|
|
187
|
+
if (view.practice && view.practice.sessions > 0) {
|
|
188
|
+
console.log(' ' + '─'.repeat(48));
|
|
189
|
+
console.log(info(` practice runs · ${view.practice.sessions} session${view.practice.sessions === 1 ? '' : 's'} · ${fmtUsd(view.practice.usd)} fabricated · not in the totals`));
|
|
190
|
+
}
|
|
191
|
+
console.log(' ' + '─'.repeat(48));
|
|
192
|
+
const totalLine = ` ${'TEAM TOTAL'.padEnd(28)} ${fmtUsd(view.teamTotal).padStart(9)}`;
|
|
193
|
+
if (view.dailyUsd !== null) {
|
|
194
|
+
const pct = Math.min(100, Math.round((view.teamTotal / view.dailyUsd) * 100));
|
|
195
|
+
console.log(totalLine + ` of ${fmtUsd(view.dailyUsd)} (${pct}%)`);
|
|
196
|
+
if (view.teamTotal >= view.dailyUsd)
|
|
197
|
+
console.log(error(' Over the team daily budget.'));
|
|
198
|
+
else if (pct >= 80)
|
|
199
|
+
console.log(info(' Approaching the team daily budget.'));
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
console.log(totalLine);
|
|
203
|
+
console.log(info(' No team budget set, `codehere team budget <usd>` to add one.'));
|
|
204
|
+
}
|
|
205
|
+
if (view.totalActions > 0) {
|
|
206
|
+
newline();
|
|
207
|
+
console.log(heading('What the agents did (recorded, integrity-checked)'));
|
|
208
|
+
console.log(' ' + '─'.repeat(48));
|
|
209
|
+
for (const m of view.machines.filter((x) => x.audit).sort((a, b) => b.audit.actions - a.audit.actions)) {
|
|
210
|
+
const status = m.audit.chainOk ? success('chain ok') : error('chain BROKEN');
|
|
211
|
+
console.log(` ${m.machineId.padEnd(28)} ${String(m.audit.actions).padStart(6)} actions ${status}`);
|
|
212
|
+
}
|
|
213
|
+
console.log(' ' + '─'.repeat(48));
|
|
214
|
+
console.log(` ${'TEAM ACTIONS'.padEnd(28)} ${String(view.totalActions).padStart(6)}`);
|
|
215
|
+
if (view.brokenChains.length)
|
|
216
|
+
console.log(error(` Integrity broken on: ${view.brokenChains.join(', ')}. Investigate before trusting that record.`));
|
|
217
|
+
}
|
|
218
|
+
if (view.tamperedShares.length || view.unsignedShares.length) {
|
|
219
|
+
newline();
|
|
220
|
+
if (view.tamperedShares.length)
|
|
221
|
+
console.log(error(` ✗ Signature/key mismatch on: ${view.tamperedShares.join(', ')} — altered, or re-signed with a different key than first seen. Don't trust that share.`));
|
|
222
|
+
if (view.unsignedShares.length)
|
|
223
|
+
console.log(info(` Unsigned, can't verify: ${view.unsignedShares.join(', ')}.`));
|
|
224
|
+
}
|
|
225
|
+
else if (view.machines.length) {
|
|
226
|
+
newline();
|
|
227
|
+
console.log(success(' ✓ All shares signed; keys match the ones first seen (TOFU) — no post-write change detected.'));
|
|
228
|
+
}
|
|
229
|
+
newline();
|
|
230
|
+
if (gitMode) {
|
|
231
|
+
const pushMsg = pushed === 'pushed'
|
|
232
|
+
? 'pushed yours'
|
|
233
|
+
: pushed === 'committed-local'
|
|
234
|
+
? 'committed yours locally (no remote or offline)'
|
|
235
|
+
: pushed === 'nochange'
|
|
236
|
+
? 'yours unchanged'
|
|
237
|
+
: 'could not push yours';
|
|
238
|
+
console.log(info('Synced:'), `git ${pulled ? 'pulled teammates' : 'pull skipped'}, ${pushMsg}. ${view.machines.length} machine(s) merged, all in your team's own repo, nothing sent to codehere.`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
console.log(info('Local-first:'), `${view.machines.length} machine(s) merged from ${cfg.dir}, codehere sent nothing to a server.`);
|
|
242
|
+
console.log(info('Tip:'), 'make the team dir a git repo with a remote and codehere will pull/push it for you.');
|
|
243
|
+
}
|
|
244
|
+
// --strict: refuse to exit clean if the record can't be trusted. A tampered
|
|
245
|
+
// signature or a broken audit chain means the team total above is not a
|
|
246
|
+
// trustworthy record — fail closed so CI / a lead's gate catches it.
|
|
247
|
+
if (strict) {
|
|
248
|
+
const bad = [...view.tamperedShares, ...view.brokenChains];
|
|
249
|
+
if (bad.length) {
|
|
250
|
+
newline();
|
|
251
|
+
console.error(error('--strict: record integrity failed.'), `Untrustworthy: ${[...new Set(bad)].join(', ')}.`);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=team-command.js.map
|