instar 0.28.74 → 0.28.76
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/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/discovery.d.ts.map +1 -1
- package/dist/commands/discovery.js +1 -0
- package/dist/commands/discovery.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +2 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/job.d.ts.map +1 -1
- package/dist/commands/job.js +1 -0
- package/dist/commands/job.js.map +1 -1
- package/dist/commands/ledgerCleanup.d.ts.map +1 -1
- package/dist/commands/ledgerCleanup.js +1 -0
- package/dist/commands/ledgerCleanup.js.map +1 -1
- package/dist/commands/listener.d.ts.map +1 -1
- package/dist/commands/listener.js +6 -0
- package/dist/commands/listener.js.map +1 -1
- package/dist/commands/nuke.d.ts.map +1 -1
- package/dist/commands/nuke.js +6 -0
- package/dist/commands/nuke.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +2 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +6 -0
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/slack-cli.d.ts.map +1 -1
- package/dist/commands/slack-cli.js +4 -0
- package/dist/commands/slack-cli.js.map +1 -1
- package/dist/commands/whatsapp.d.ts.map +1 -1
- package/dist/commands/whatsapp.js +1 -0
- package/dist/commands/whatsapp.js.map +1 -1
- package/dist/commands/worktree.d.ts.map +1 -1
- package/dist/commands/worktree.js +1 -0
- package/dist/commands/worktree.js.map +1 -1
- package/dist/core/AgentConnector.d.ts.map +1 -1
- package/dist/core/AgentConnector.js +3 -0
- package/dist/core/AgentConnector.js.map +1 -1
- package/dist/core/AgentRegistry.d.ts.map +1 -1
- package/dist/core/AgentRegistry.js +2 -0
- package/dist/core/AgentRegistry.js.map +1 -1
- package/dist/core/AutoDispatcher.d.ts.map +1 -1
- package/dist/core/AutoDispatcher.js +1 -0
- package/dist/core/AutoDispatcher.js.map +1 -1
- package/dist/core/AutoUpdater.d.ts.map +1 -1
- package/dist/core/AutoUpdater.js +1 -0
- package/dist/core/AutoUpdater.js.map +1 -1
- package/dist/core/AutonomousEvolution.d.ts.map +1 -1
- package/dist/core/AutonomousEvolution.js +1 -0
- package/dist/core/AutonomousEvolution.js.map +1 -1
- package/dist/core/BackupManager.d.ts.map +1 -1
- package/dist/core/BackupManager.js +1 -0
- package/dist/core/BackupManager.js.map +1 -1
- package/dist/core/BranchManager.d.ts.map +1 -1
- package/dist/core/BranchManager.js +3 -0
- package/dist/core/BranchManager.js.map +1 -1
- package/dist/core/CaffeinateManager.d.ts.map +1 -1
- package/dist/core/CaffeinateManager.js +1 -0
- package/dist/core/CaffeinateManager.js.map +1 -1
- package/dist/core/DeferredDispatchTracker.d.ts.map +1 -1
- package/dist/core/DeferredDispatchTracker.js +1 -0
- package/dist/core/DeferredDispatchTracker.js.map +1 -1
- package/dist/core/DispatchManager.d.ts.map +1 -1
- package/dist/core/DispatchManager.js +2 -0
- package/dist/core/DispatchManager.js.map +1 -1
- package/dist/core/EvolutionManager.d.ts.map +1 -1
- package/dist/core/EvolutionManager.js +1 -0
- package/dist/core/EvolutionManager.js.map +1 -1
- package/dist/core/ExecutionJournal.d.ts.map +1 -1
- package/dist/core/ExecutionJournal.js +1 -0
- package/dist/core/ExecutionJournal.js.map +1 -1
- package/dist/core/FeedbackManager.d.ts.map +1 -1
- package/dist/core/FeedbackManager.js +1 -0
- package/dist/core/FeedbackManager.js.map +1 -1
- package/dist/core/FileClassifier.d.ts.map +1 -1
- package/dist/core/FileClassifier.js +4 -0
- package/dist/core/FileClassifier.js.map +1 -1
- package/dist/core/ForegroundRestartWatcher.d.ts.map +1 -1
- package/dist/core/ForegroundRestartWatcher.js +2 -0
- package/dist/core/ForegroundRestartWatcher.js.map +1 -1
- package/dist/core/GitStateManager.d.ts.map +1 -1
- package/dist/core/GitStateManager.js +1 -0
- package/dist/core/GitStateManager.js.map +1 -1
- package/dist/core/GitSync.d.ts.map +1 -1
- package/dist/core/GitSync.js +5 -0
- package/dist/core/GitSync.js.map +1 -1
- package/dist/core/GlobalInstallCleanup.d.ts.map +1 -1
- package/dist/core/GlobalInstallCleanup.js +2 -0
- package/dist/core/GlobalInstallCleanup.js.map +1 -1
- package/dist/core/GlobalSecretStore.d.ts.map +1 -1
- package/dist/core/GlobalSecretStore.js +2 -0
- package/dist/core/GlobalSecretStore.js.map +1 -1
- package/dist/core/HandoffManager.d.ts.map +1 -1
- package/dist/core/HandoffManager.js +4 -0
- package/dist/core/HandoffManager.js.map +1 -1
- package/dist/core/LedgerSessionRegistry.d.ts.map +1 -1
- package/dist/core/LedgerSessionRegistry.js +1 -0
- package/dist/core/LedgerSessionRegistry.js.map +1 -1
- package/dist/core/MachineIdentity.d.ts.map +1 -1
- package/dist/core/MachineIdentity.js +1 -0
- package/dist/core/MachineIdentity.js.map +1 -1
- package/dist/core/ParallelDevWiring.d.ts.map +1 -1
- package/dist/core/ParallelDevWiring.js +1 -0
- package/dist/core/ParallelDevWiring.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +2 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/ProjectMapper.d.ts.map +1 -1
- package/dist/core/ProjectMapper.js +2 -0
- package/dist/core/ProjectMapper.js.map +1 -1
- package/dist/core/RelationshipManager.d.ts.map +1 -1
- package/dist/core/RelationshipManager.js +2 -0
- package/dist/core/RelationshipManager.js.map +1 -1
- package/dist/core/SafeFsExecutor.d.ts +41 -0
- package/dist/core/SafeFsExecutor.d.ts.map +1 -0
- package/dist/core/SafeFsExecutor.js +146 -0
- package/dist/core/SafeFsExecutor.js.map +1 -0
- package/dist/core/SafeGitExecutor.d.ts +139 -0
- package/dist/core/SafeGitExecutor.d.ts.map +1 -0
- package/dist/core/SafeGitExecutor.js +631 -0
- package/dist/core/SafeGitExecutor.js.map +1 -0
- package/dist/core/ScopeVerifier.d.ts.map +1 -1
- package/dist/core/ScopeVerifier.js +1 -0
- package/dist/core/ScopeVerifier.js.map +1 -1
- package/dist/core/SecretStore.d.ts.map +1 -1
- package/dist/core/SecretStore.js +1 -0
- package/dist/core/SecretStore.js.map +1 -1
- package/dist/core/SharedStateLedger.d.ts.map +1 -1
- package/dist/core/SharedStateLedger.js +1 -0
- package/dist/core/SharedStateLedger.js.map +1 -1
- package/dist/core/SoulManager.d.ts.map +1 -1
- package/dist/core/SoulManager.js +2 -0
- package/dist/core/SoulManager.js.map +1 -1
- package/dist/core/SourceTreeGuard.d.ts +69 -0
- package/dist/core/SourceTreeGuard.d.ts.map +1 -0
- package/dist/core/SourceTreeGuard.js +378 -0
- package/dist/core/SourceTreeGuard.js.map +1 -0
- package/dist/core/StateManager.d.ts.map +1 -1
- package/dist/core/StateManager.js +49 -9
- package/dist/core/StateManager.js.map +1 -1
- package/dist/core/SyncOrchestrator.d.ts.map +1 -1
- package/dist/core/SyncOrchestrator.js +3 -0
- package/dist/core/SyncOrchestrator.js.map +1 -1
- package/dist/core/UpdateChecker.d.ts.map +1 -1
- package/dist/core/UpdateChecker.js +2 -0
- package/dist/core/UpdateChecker.js.map +1 -1
- package/dist/core/UpgradeGuideProcessor.d.ts.map +1 -1
- package/dist/core/UpgradeGuideProcessor.js +2 -0
- package/dist/core/UpgradeGuideProcessor.js.map +1 -1
- package/dist/core/WorktreeManager.d.ts.map +1 -1
- package/dist/core/WorktreeManager.js +7 -0
- package/dist/core/WorktreeManager.js.map +1 -1
- package/dist/knowledge/KnowledgeManager.d.ts.map +1 -1
- package/dist/knowledge/KnowledgeManager.js +1 -0
- package/dist/knowledge/KnowledgeManager.js.map +1 -1
- package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
- package/dist/lifeline/ServerSupervisor.js +14 -0
- package/dist/lifeline/ServerSupervisor.js.map +1 -1
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
- package/dist/lifeline/TelegramLifeline.js +1 -0
- package/dist/lifeline/TelegramLifeline.js.map +1 -1
- package/dist/lifeline/droppedMessages.d.ts.map +1 -1
- package/dist/lifeline/droppedMessages.js +1 -0
- package/dist/lifeline/droppedMessages.js.map +1 -1
- package/dist/memory/EpisodicMemory.d.ts.map +1 -1
- package/dist/memory/EpisodicMemory.js +1 -0
- package/dist/memory/EpisodicMemory.js.map +1 -1
- package/dist/memory/TopicMemory.d.ts.map +1 -1
- package/dist/memory/TopicMemory.js +4 -0
- package/dist/memory/TopicMemory.js.map +1 -1
- package/dist/messaging/AgentTokenManager.d.ts.map +1 -1
- package/dist/messaging/AgentTokenManager.js +1 -0
- package/dist/messaging/AgentTokenManager.js.map +1 -1
- package/dist/messaging/DropPickup.js +1 -0
- package/dist/messaging/DropPickup.js.map +1 -1
- package/dist/messaging/GitSyncTransport.d.ts.map +1 -1
- package/dist/messaging/GitSyncTransport.js +3 -0
- package/dist/messaging/GitSyncTransport.js.map +1 -1
- package/dist/messaging/MessageStore.d.ts.map +1 -1
- package/dist/messaging/MessageStore.js +2 -0
- package/dist/messaging/MessageStore.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +4 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/messaging/backends/BaileysBackend.d.ts.map +1 -1
- package/dist/messaging/backends/BaileysBackend.js +2 -0
- package/dist/messaging/backends/BaileysBackend.js.map +1 -1
- package/dist/messaging/shared/EncryptedAuthStore.d.ts.map +1 -1
- package/dist/messaging/shared/EncryptedAuthStore.js +2 -0
- package/dist/messaging/shared/EncryptedAuthStore.js.map +1 -1
- package/dist/messaging/shared/MessageLogger.d.ts.map +1 -1
- package/dist/messaging/shared/MessageLogger.js +1 -0
- package/dist/messaging/shared/MessageLogger.js.map +1 -1
- package/dist/messaging/shared/PrivacyConsent.d.ts.map +1 -1
- package/dist/messaging/shared/PrivacyConsent.js +1 -0
- package/dist/messaging/shared/PrivacyConsent.js.map +1 -1
- package/dist/messaging/shared/SessionChannelRegistry.d.ts.map +1 -1
- package/dist/messaging/shared/SessionChannelRegistry.js +1 -0
- package/dist/messaging/shared/SessionChannelRegistry.js.map +1 -1
- package/dist/moltbridge/ProfileCompiler.d.ts.map +1 -1
- package/dist/moltbridge/ProfileCompiler.js +3 -0
- package/dist/moltbridge/ProfileCompiler.js.map +1 -1
- package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
- package/dist/monitoring/CommitmentTracker.js +1 -0
- package/dist/monitoring/CommitmentTracker.js.map +1 -1
- package/dist/monitoring/CredentialProvider.d.ts.map +1 -1
- package/dist/monitoring/CredentialProvider.js +1 -0
- package/dist/monitoring/CredentialProvider.js.map +1 -1
- package/dist/monitoring/HealthChecker.d.ts.map +1 -1
- package/dist/monitoring/HealthChecker.js +1 -0
- package/dist/monitoring/HealthChecker.js.map +1 -1
- package/dist/monitoring/HookEventReceiver.d.ts.map +1 -1
- package/dist/monitoring/HookEventReceiver.js +1 -0
- package/dist/monitoring/HookEventReceiver.js.map +1 -1
- package/dist/monitoring/InstructionsVerifier.d.ts.map +1 -1
- package/dist/monitoring/InstructionsVerifier.js +1 -0
- package/dist/monitoring/InstructionsVerifier.js.map +1 -1
- package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
- package/dist/monitoring/PresenceProxy.js +4 -0
- package/dist/monitoring/PresenceProxy.js.map +1 -1
- package/dist/monitoring/QuotaTracker.d.ts.map +1 -1
- package/dist/monitoring/QuotaTracker.js +1 -0
- package/dist/monitoring/QuotaTracker.js.map +1 -1
- package/dist/monitoring/SessionMigrator.d.ts.map +1 -1
- package/dist/monitoring/SessionMigrator.js +1 -0
- package/dist/monitoring/SessionMigrator.js.map +1 -1
- package/dist/monitoring/SessionRecovery.d.ts.map +1 -1
- package/dist/monitoring/SessionRecovery.js +1 -0
- package/dist/monitoring/SessionRecovery.js.map +1 -1
- package/dist/monitoring/TelemetryAuth.d.ts.map +1 -1
- package/dist/monitoring/TelemetryAuth.js +2 -0
- package/dist/monitoring/TelemetryAuth.js.map +1 -1
- package/dist/monitoring/TriageOrchestrator.d.ts.map +1 -1
- package/dist/monitoring/TriageOrchestrator.js +2 -0
- package/dist/monitoring/TriageOrchestrator.js.map +1 -1
- package/dist/monitoring/WorktreeReaper.d.ts.map +1 -1
- package/dist/monitoring/WorktreeReaper.js +3 -0
- package/dist/monitoring/WorktreeReaper.js.map +1 -1
- package/dist/monitoring/probes/PlatformProbe.d.ts.map +1 -1
- package/dist/monitoring/probes/PlatformProbe.js +2 -0
- package/dist/monitoring/probes/PlatformProbe.js.map +1 -1
- package/dist/paste/PasteManager.d.ts.map +1 -1
- package/dist/paste/PasteManager.js +4 -0
- package/dist/paste/PasteManager.js.map +1 -1
- package/dist/publishing/PrivateViewer.d.ts.map +1 -1
- package/dist/publishing/PrivateViewer.js +1 -0
- package/dist/publishing/PrivateViewer.js.map +1 -1
- package/dist/scheduler/JobScheduler.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.js +1 -0
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +21 -9
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/AgentDiscovery.d.ts.map +1 -1
- package/dist/threadline/AgentDiscovery.js +1 -0
- package/dist/threadline/AgentDiscovery.js.map +1 -1
- package/dist/threadline/AgentTrustManager.d.ts.map +1 -1
- package/dist/threadline/AgentTrustManager.js +1 -0
- package/dist/threadline/AgentTrustManager.js.map +1 -1
- package/dist/threadline/CircuitBreaker.d.ts.map +1 -1
- package/dist/threadline/CircuitBreaker.js +1 -0
- package/dist/threadline/CircuitBreaker.js.map +1 -1
- package/dist/threadline/ComputeMeter.d.ts.map +1 -1
- package/dist/threadline/ComputeMeter.js +1 -0
- package/dist/threadline/ComputeMeter.js.map +1 -1
- package/dist/threadline/ContextThreadMap.d.ts.map +1 -1
- package/dist/threadline/ContextThreadMap.js +1 -0
- package/dist/threadline/ContextThreadMap.js.map +1 -1
- package/dist/threadline/InvitationManager.d.ts.map +1 -1
- package/dist/threadline/InvitationManager.js +1 -0
- package/dist/threadline/InvitationManager.js.map +1 -1
- package/dist/threadline/MCPAuth.d.ts.map +1 -1
- package/dist/threadline/MCPAuth.js +1 -0
- package/dist/threadline/MCPAuth.js.map +1 -1
- package/dist/threadline/PipeSessionSpawner.d.ts.map +1 -1
- package/dist/threadline/PipeSessionSpawner.js +2 -0
- package/dist/threadline/PipeSessionSpawner.js.map +1 -1
- package/dist/threadline/RateLimiter.d.ts.map +1 -1
- package/dist/threadline/RateLimiter.js +1 -0
- package/dist/threadline/RateLimiter.js.map +1 -1
- package/dist/threadline/SessionLifecycle.d.ts.map +1 -1
- package/dist/threadline/SessionLifecycle.js +1 -0
- package/dist/threadline/SessionLifecycle.js.map +1 -1
- package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
- package/dist/threadline/ThreadlineBootstrap.js +1 -0
- package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
- package/dist/threadline/WakeSocketServer.d.ts.map +1 -1
- package/dist/threadline/WakeSocketServer.js +2 -0
- package/dist/threadline/WakeSocketServer.js.map +1 -1
- package/dist/threadline/listener-daemon.d.ts.map +1 -1
- package/dist/threadline/listener-daemon.js +2 -0
- package/dist/threadline/listener-daemon.js.map +1 -1
- package/dist/users/UserManager.d.ts.map +1 -1
- package/dist/users/UserManager.js +1 -0
- package/dist/users/UserManager.js.map +1 -1
- package/dist/users/UserOnboarding.d.ts.map +1 -1
- package/dist/users/UserOnboarding.js +1 -0
- package/dist/users/UserOnboarding.js.map +1 -1
- package/dist/utils/jsonl-rotation.d.ts.map +1 -1
- package/dist/utils/jsonl-rotation.js +1 -0
- package/dist/utils/jsonl-rotation.js.map +1 -1
- package/package.json +4 -2
- package/scripts/add-migration-marker.js +121 -0
- package/scripts/analyze-release.js +6 -0
- package/scripts/check-contract-evidence.js +2 -0
- package/scripts/destructive-command-shim.js +1 -0
- package/scripts/fix-better-sqlite3.cjs +2 -0
- package/scripts/generate-builtin-manifest.cjs +1 -0
- package/scripts/instar-dev-precommit.js +2 -0
- package/scripts/lint-no-direct-destructive.js +597 -0
- package/scripts/migrate-incident-2026-04-17.mjs +1 -0
- package/scripts/pre-push-gate.js +24 -0
- package/scripts/test-bootstrap-relay.mjs +1 -0
- package/scripts/worktree-commit-msg-hook.js +4 -0
- package/scripts/worktree-precommit-gate.js +1 -0
- package/src/data/builtin-manifest.json +98 -98
- package/src/templates/scripts/git-sync-gate.sh +4 -0
- package/upgrades/0.28.75.md +29 -0
- package/upgrades/0.28.76.md +67 -0
- package/upgrades/side-effects/0.28.75.md +53 -0
- package/upgrades/side-effects/comprehensive-destructive-tool-containment-foundation.md +74 -0
- package/upgrades/side-effects/source-tree-guard.md +340 -0
- package/upgrades/side-effects/telegram-lifeline-version-missing-info.md +76 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-no-direct-destructive.js — refuses direct destructive git/fs callsites.
|
|
4
|
+
*
|
|
5
|
+
* Implements AC-3 / AC-5 / AC-6 / AC-7 from
|
|
6
|
+
* docs/specs/COMPREHENSIVE-DESTRUCTIVE-TOOL-CONTAINMENT-SPEC.md.
|
|
7
|
+
*
|
|
8
|
+
* The funnels (`src/core/SafeGitExecutor.ts`, `src/core/SafeFsExecutor.ts`)
|
|
9
|
+
* are the only modules in the codebase allowed to call:
|
|
10
|
+
* - `child_process.execFileSync('git', ...)` / `execSync('git ...')` /
|
|
11
|
+
* `spawn('git', ...)` / `spawnSync('git', ...)` / `exec('git ...')`
|
|
12
|
+
* - `fs.rm` / `fs.rmSync` / `fs.unlink` / `fs.unlinkSync` / `fs.rmdir` /
|
|
13
|
+
* `fs.rmdirSync` (and their `fs/promises` counterparts)
|
|
14
|
+
* - `simpleGit(...)` from the `simple-git` package
|
|
15
|
+
*
|
|
16
|
+
* This script AST-walks every `.ts`/`.tsx`/`.js`/`.mjs`/`.cjs` file in
|
|
17
|
+
* `src/`, `tests/`, `scripts/` (configurable via CLI args) and flags
|
|
18
|
+
* violations.
|
|
19
|
+
*
|
|
20
|
+
* It also greps `.sh` files and the `scripts` section of `package.json`
|
|
21
|
+
* for direct destructive `git <verb>` invocations (closed verb list).
|
|
22
|
+
*
|
|
23
|
+
* Exit codes:
|
|
24
|
+
* 0 — no violations.
|
|
25
|
+
* 1 — at least one violation.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* node scripts/lint-no-direct-destructive.js # full repo
|
|
29
|
+
* node scripts/lint-no-direct-destructive.js --staged # staged files only
|
|
30
|
+
* node scripts/lint-no-direct-destructive.js path1 path2 # specific files
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import fs from 'node:fs';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
import { execSync } from 'node:child_process';
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
|
+
|
|
38
|
+
// Avoid importing typescript at module top — it's a heavy dep. We require it
|
|
39
|
+
// only when a TS file actually needs parsing.
|
|
40
|
+
let _ts = null;
|
|
41
|
+
function ts() {
|
|
42
|
+
if (_ts) return _ts;
|
|
43
|
+
_ts = require('typescript');
|
|
44
|
+
return _ts;
|
|
45
|
+
}
|
|
46
|
+
import { createRequire } from 'node:module';
|
|
47
|
+
const require = createRequire(import.meta.url);
|
|
48
|
+
|
|
49
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
50
|
+
const __dirname = path.dirname(__filename);
|
|
51
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
52
|
+
|
|
53
|
+
// ── Allowlist (closed) ─────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Files that may legitimately call destructive git/fs primitives directly.
|
|
57
|
+
* Adding entries requires a spec change.
|
|
58
|
+
*/
|
|
59
|
+
const ALLOWLIST = new Set([
|
|
60
|
+
'src/core/SafeGitExecutor.ts',
|
|
61
|
+
'src/core/SafeFsExecutor.ts',
|
|
62
|
+
'tests/unit/SafeGitExecutor.test.ts',
|
|
63
|
+
'tests/unit/SafeFsExecutor.test.ts',
|
|
64
|
+
// Transitional: tracked under commitment://incremental-migration (due
|
|
65
|
+
// 2026-05-03). PR #2 migrates these fs.unlinkSync calls through
|
|
66
|
+
// SafeFsExecutor and removes them from this list.
|
|
67
|
+
'src/messaging/imessage/IMessageAdapter.ts',
|
|
68
|
+
'src/messaging/imessage/NativeBackend.ts',
|
|
69
|
+
// The shim runs `git <verb> --dry-run` first to count files, then re-invokes
|
|
70
|
+
// for real. Both invocations route through SafeGitExecutor, but the shim's
|
|
71
|
+
// own implementation file must be allowed to import the executor.
|
|
72
|
+
// The shim itself uses SafeGitExecutor — no direct execFileSync needed. If
|
|
73
|
+
// the shim ever needs direct access it gets added here.
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Allow `// safe-git-allow: <reason>` as a per-file escape on the FIRST
|
|
78
|
+
* non-empty line of the file. Used by SafeGitExecutor.ts itself and the
|
|
79
|
+
* test files. Other callers must be on the closed allowlist.
|
|
80
|
+
*/
|
|
81
|
+
function hasAllowComment(text) {
|
|
82
|
+
const lines = text.split('\n').slice(0, 5);
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
if (/^\s*\/\/\s*safe-git-allow:/.test(line)) return true;
|
|
85
|
+
if (/^\s*\/\*[\s\S]*?safe-git-allow:/m.test(line)) return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Violation reporting ────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const violations = [];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Per-callsite marker: `// safe-git-allow: incremental-migration` placed on
|
|
96
|
+
* the line immediately above (or at end of) a flagged callsite suppresses
|
|
97
|
+
* that single violation. Used during the transitional period before
|
|
98
|
+
* commitment://incremental-migration lands (PR #2). Expires 2026-05-03.
|
|
99
|
+
*
|
|
100
|
+
* NEW callsites cannot use this marker — only pre-existing callsites that
|
|
101
|
+
* were stamped in PR #1 by scripts/add-migration-marker.js. After PR #2,
|
|
102
|
+
* zero markers should remain.
|
|
103
|
+
*/
|
|
104
|
+
const MIGRATION_MARKER_EXPIRY = new Date('2026-05-03T00:00:00Z');
|
|
105
|
+
const MIGRATION_MARKER_RE = /\/\/\s*safe-git-allow:\s*incremental-migration\b/;
|
|
106
|
+
|
|
107
|
+
function hasLineMarker(text, line) {
|
|
108
|
+
// line is 1-based. Check the line itself (trailing comment) and the
|
|
109
|
+
// line immediately above it.
|
|
110
|
+
const lines = text.split('\n');
|
|
111
|
+
const idx = line - 1;
|
|
112
|
+
if (idx < 0 || idx >= lines.length) return false;
|
|
113
|
+
if (MIGRATION_MARKER_RE.test(lines[idx])) return true;
|
|
114
|
+
if (idx - 1 >= 0 && MIGRATION_MARKER_RE.test(lines[idx - 1])) return true;
|
|
115
|
+
// Also allow the marker two lines above to tolerate blank-line spacing.
|
|
116
|
+
if (idx - 2 >= 0 && /^\s*$/.test(lines[idx - 1]) && MIGRATION_MARKER_RE.test(lines[idx - 2])) return true;
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function migrationMarkerExpired() {
|
|
121
|
+
return new Date() >= MIGRATION_MARKER_EXPIRY;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function migrationMarkerDisabled() {
|
|
125
|
+
// Internal flag used by scripts/add-migration-marker.js to collect ALL
|
|
126
|
+
// pre-existing violations (including ones that already carry the marker)
|
|
127
|
+
// so it can re-stamp idempotently.
|
|
128
|
+
return process.env.INSTAR_DISABLE_MIGRATION_MARKER === '1';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function report(file, line, col, msg, ctx) {
|
|
132
|
+
// Honor per-callsite incremental-migration marker (transitional period).
|
|
133
|
+
if (
|
|
134
|
+
ctx &&
|
|
135
|
+
ctx.text &&
|
|
136
|
+
!migrationMarkerDisabled() &&
|
|
137
|
+
!migrationMarkerExpired() &&
|
|
138
|
+
hasLineMarker(ctx.text, line)
|
|
139
|
+
) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
violations.push({ file, line, col, msg });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── AST scan: TS / JS files ────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
const DESTRUCTIVE_FS_NAMES = new Set([
|
|
148
|
+
'rm',
|
|
149
|
+
'rmSync',
|
|
150
|
+
'unlink',
|
|
151
|
+
'unlinkSync',
|
|
152
|
+
'rmdir',
|
|
153
|
+
'rmdirSync',
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
const CHILD_PROCESS_FNS = new Set([
|
|
157
|
+
'execFileSync',
|
|
158
|
+
'execSync',
|
|
159
|
+
'spawn',
|
|
160
|
+
'spawnSync',
|
|
161
|
+
'exec',
|
|
162
|
+
'execFile',
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
const CHILD_PROCESS_MODULE_NAMES = new Set([
|
|
166
|
+
'child_process',
|
|
167
|
+
'node:child_process',
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const FS_MODULE_NAMES = new Set([
|
|
171
|
+
'fs',
|
|
172
|
+
'node:fs',
|
|
173
|
+
'fs/promises',
|
|
174
|
+
'node:fs/promises',
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Walk a TypeScript SourceFile AST, collecting violations.
|
|
179
|
+
*
|
|
180
|
+
* Rules:
|
|
181
|
+
* 1. Direct call to one of CHILD_PROCESS_FNS where the first arg is the
|
|
182
|
+
* string literal 'git' OR a string literal starting with 'git ' — flag.
|
|
183
|
+
* 2. Member access on an identifier known to alias the child_process or
|
|
184
|
+
* fs module — same rule applies.
|
|
185
|
+
* 3. Named import of `simpleGit` from `simple-git` — flag.
|
|
186
|
+
* 4. Named import of one of DESTRUCTIVE_FS_NAMES from fs / fs/promises — flag.
|
|
187
|
+
* 5. Namespace import of fs / fs/promises followed by member-access
|
|
188
|
+
* to a destructive name — flag.
|
|
189
|
+
* 6. require('child_process').execFileSync('git', ...) and equivalents — flag.
|
|
190
|
+
* 7. Dynamic property access used to evade the rule (`fs['rm' + 'Sync']`) — flag.
|
|
191
|
+
*/
|
|
192
|
+
function lintTsFile(file, text) {
|
|
193
|
+
const T = ts();
|
|
194
|
+
const sf = T.createSourceFile(file, text, T.ScriptTarget.Latest, true);
|
|
195
|
+
const ctx = { text };
|
|
196
|
+
const r = (f, l, c, m) => report(f, l, c, m, ctx);
|
|
197
|
+
|
|
198
|
+
/** module name → local identifier (default + namespace + named) */
|
|
199
|
+
const childProcessIdentifiers = new Set();
|
|
200
|
+
const childProcessNamedImports = new Map(); // localName -> originalName
|
|
201
|
+
const fsNamespaceIdentifiers = new Set();
|
|
202
|
+
const fsNamedDestructiveImports = new Map(); // localName -> originalName
|
|
203
|
+
const simpleGitImports = []; // {localName}
|
|
204
|
+
const requireBindings = new Map(); // localName -> moduleName (if literal)
|
|
205
|
+
|
|
206
|
+
function lineCol(node) {
|
|
207
|
+
const lc = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
208
|
+
return { line: lc.line + 1, col: lc.character + 1 };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function visit(node) {
|
|
212
|
+
// ── Imports ────────────────────────────────────────────────
|
|
213
|
+
if (T.isImportDeclaration(node) && node.moduleSpecifier && T.isStringLiteral(node.moduleSpecifier)) {
|
|
214
|
+
const mod = node.moduleSpecifier.text;
|
|
215
|
+
const ic = node.importClause;
|
|
216
|
+
if (!ic) return;
|
|
217
|
+
if (ic.namedBindings) {
|
|
218
|
+
if (T.isNamespaceImport(ic.namedBindings)) {
|
|
219
|
+
const localName = ic.namedBindings.name.text;
|
|
220
|
+
if (CHILD_PROCESS_MODULE_NAMES.has(mod)) {
|
|
221
|
+
childProcessIdentifiers.add(localName);
|
|
222
|
+
} else if (FS_MODULE_NAMES.has(mod)) {
|
|
223
|
+
fsNamespaceIdentifiers.add(localName);
|
|
224
|
+
}
|
|
225
|
+
} else if (T.isNamedImports(ic.namedBindings)) {
|
|
226
|
+
for (const el of ic.namedBindings.elements) {
|
|
227
|
+
const importedName = el.propertyName ? el.propertyName.text : el.name.text;
|
|
228
|
+
const localName = el.name.text;
|
|
229
|
+
if (CHILD_PROCESS_MODULE_NAMES.has(mod) && CHILD_PROCESS_FNS.has(importedName)) {
|
|
230
|
+
childProcessNamedImports.set(localName, importedName);
|
|
231
|
+
} else if (FS_MODULE_NAMES.has(mod) && DESTRUCTIVE_FS_NAMES.has(importedName)) {
|
|
232
|
+
fsNamedDestructiveImports.set(localName, importedName);
|
|
233
|
+
} else if (mod === 'simple-git' && importedName === 'simpleGit') {
|
|
234
|
+
simpleGitImports.push({ localName });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Default import — `import fs from 'node:fs'` style.
|
|
240
|
+
if (ic.name) {
|
|
241
|
+
const localName = ic.name.text;
|
|
242
|
+
if (CHILD_PROCESS_MODULE_NAMES.has(mod)) {
|
|
243
|
+
childProcessIdentifiers.add(localName);
|
|
244
|
+
} else if (FS_MODULE_NAMES.has(mod)) {
|
|
245
|
+
fsNamespaceIdentifiers.add(localName);
|
|
246
|
+
} else if (mod === 'simple-git') {
|
|
247
|
+
simpleGitImports.push({ localName });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── require('child_process') / require('fs') ───────────────
|
|
253
|
+
if (T.isVariableDeclaration(node) && node.initializer && T.isCallExpression(node.initializer)) {
|
|
254
|
+
const callee = node.initializer.expression;
|
|
255
|
+
if (T.isIdentifier(callee) && callee.text === 'require' && node.initializer.arguments.length === 1) {
|
|
256
|
+
const arg = node.initializer.arguments[0];
|
|
257
|
+
if (T.isStringLiteral(arg)) {
|
|
258
|
+
const mod = arg.text;
|
|
259
|
+
if (T.isIdentifier(node.name)) {
|
|
260
|
+
const localName = node.name.text;
|
|
261
|
+
if (CHILD_PROCESS_MODULE_NAMES.has(mod)) {
|
|
262
|
+
childProcessIdentifiers.add(localName);
|
|
263
|
+
requireBindings.set(localName, mod);
|
|
264
|
+
} else if (FS_MODULE_NAMES.has(mod)) {
|
|
265
|
+
fsNamespaceIdentifiers.add(localName);
|
|
266
|
+
requireBindings.set(localName, mod);
|
|
267
|
+
} else if (mod === 'simple-git') {
|
|
268
|
+
simpleGitImports.push({ localName });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Destructured: const { execFileSync } = require('child_process')
|
|
272
|
+
if (T.isObjectBindingPattern(node.name)) {
|
|
273
|
+
for (const el of node.name.elements) {
|
|
274
|
+
const importedName = el.propertyName && T.isIdentifier(el.propertyName)
|
|
275
|
+
? el.propertyName.text
|
|
276
|
+
: (T.isIdentifier(el.name) ? el.name.text : null);
|
|
277
|
+
const localName = T.isIdentifier(el.name) ? el.name.text : null;
|
|
278
|
+
if (!importedName || !localName) continue;
|
|
279
|
+
if (CHILD_PROCESS_MODULE_NAMES.has(mod) && CHILD_PROCESS_FNS.has(importedName)) {
|
|
280
|
+
childProcessNamedImports.set(localName, importedName);
|
|
281
|
+
} else if (FS_MODULE_NAMES.has(mod) && DESTRUCTIVE_FS_NAMES.has(importedName)) {
|
|
282
|
+
fsNamedDestructiveImports.set(localName, importedName);
|
|
283
|
+
} else if (mod === 'simple-git' && importedName === 'simpleGit') {
|
|
284
|
+
simpleGitImports.push({ localName });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Inline require('child_process').execFileSync(...) ──────
|
|
293
|
+
if (T.isCallExpression(node) && T.isPropertyAccessExpression(node.expression)) {
|
|
294
|
+
const obj = node.expression.expression;
|
|
295
|
+
const name = node.expression.name.text;
|
|
296
|
+
if (T.isCallExpression(obj) && T.isIdentifier(obj.expression) && obj.expression.text === 'require'
|
|
297
|
+
&& obj.arguments.length === 1 && T.isStringLiteral(obj.arguments[0])) {
|
|
298
|
+
const mod = obj.arguments[0].text;
|
|
299
|
+
if (CHILD_PROCESS_MODULE_NAMES.has(mod) && CHILD_PROCESS_FNS.has(name)) {
|
|
300
|
+
if (firstArgIsGit(node)) {
|
|
301
|
+
const lc = lineCol(node);
|
|
302
|
+
r(file, lc.line, lc.col, `Direct require('${mod}').${name}('git', ...) — use SafeGitExecutor.`);
|
|
303
|
+
}
|
|
304
|
+
} else if (FS_MODULE_NAMES.has(mod) && DESTRUCTIVE_FS_NAMES.has(name)) {
|
|
305
|
+
const lc = lineCol(node);
|
|
306
|
+
r(file, lc.line, lc.col, `Direct require('${mod}').${name}(...) — use SafeFsExecutor.`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Plain CallExpression of an identifier ──────────────────
|
|
312
|
+
if (T.isCallExpression(node) && T.isIdentifier(node.expression)) {
|
|
313
|
+
const fnName = node.expression.text;
|
|
314
|
+
// Named import alias for execFileSync/etc.
|
|
315
|
+
if (childProcessNamedImports.has(fnName)) {
|
|
316
|
+
if (firstArgIsGit(node)) {
|
|
317
|
+
const lc = lineCol(node);
|
|
318
|
+
r(file, lc.line, lc.col, `Direct ${childProcessNamedImports.get(fnName)}('git', ...) — use SafeGitExecutor.`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Named import alias for rm/etc.
|
|
322
|
+
if (fsNamedDestructiveImports.has(fnName)) {
|
|
323
|
+
const lc = lineCol(node);
|
|
324
|
+
r(file, lc.line, lc.col, `Direct ${fsNamedDestructiveImports.get(fnName)}(...) — use SafeFsExecutor.`);
|
|
325
|
+
}
|
|
326
|
+
// simpleGit() call.
|
|
327
|
+
if (simpleGitImports.some((s) => s.localName === fnName)) {
|
|
328
|
+
const lc = lineCol(node);
|
|
329
|
+
r(file, lc.line, lc.col, `Direct simpleGit(...) — use SafeGitExecutor.`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── Member call on namespace identifier ────────────────────
|
|
334
|
+
if (T.isCallExpression(node) && T.isPropertyAccessExpression(node.expression)) {
|
|
335
|
+
const obj = node.expression.expression;
|
|
336
|
+
const name = node.expression.name.text;
|
|
337
|
+
if (T.isIdentifier(obj)) {
|
|
338
|
+
const objName = obj.text;
|
|
339
|
+
if (childProcessIdentifiers.has(objName) && CHILD_PROCESS_FNS.has(name)) {
|
|
340
|
+
if (firstArgIsGit(node)) {
|
|
341
|
+
const lc = lineCol(node);
|
|
342
|
+
r(file, lc.line, lc.col, `Direct ${objName}.${name}('git', ...) — use SafeGitExecutor.`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (fsNamespaceIdentifiers.has(objName) && DESTRUCTIVE_FS_NAMES.has(name)) {
|
|
346
|
+
const lc = lineCol(node);
|
|
347
|
+
r(file, lc.line, lc.col, `Direct ${objName}.${name}(...) — use SafeFsExecutor.`);
|
|
348
|
+
}
|
|
349
|
+
// Defense-in-depth: catch fs.promises.rm via fs identifier.
|
|
350
|
+
if (fsNamespaceIdentifiers.has(objName) && name === 'promises') {
|
|
351
|
+
// The next member access in a chained call.
|
|
352
|
+
// Detected via the parent CallExpression where expression is
|
|
353
|
+
// `fs.promises.rm(...)` — the whole chain is this PropertyAccessExpression.
|
|
354
|
+
// We already capture this via the ElementAccessExpression branch below
|
|
355
|
+
// and via deep walking, but most simply: a CallExpression whose
|
|
356
|
+
// callee is `<fs>.promises.<destructive>` deserves a flag.
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// fs.promises.rm(...) — the callee is PropertyAccess(PropertyAccess(fs, promises), rm)
|
|
360
|
+
if (T.isPropertyAccessExpression(obj) && T.isIdentifier(obj.expression)
|
|
361
|
+
&& fsNamespaceIdentifiers.has(obj.expression.text)
|
|
362
|
+
&& obj.name.text === 'promises'
|
|
363
|
+
&& DESTRUCTIVE_FS_NAMES.has(name)) {
|
|
364
|
+
const lc = lineCol(node);
|
|
365
|
+
r(file, lc.line, lc.col, `Direct ${obj.expression.text}.promises.${name}(...) — use SafeFsExecutor.`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ── Dynamic / computed access: fs['rm' + 'Sync'](...) ──────
|
|
370
|
+
if (T.isCallExpression(node) && T.isElementAccessExpression(node.expression)) {
|
|
371
|
+
const obj = node.expression.expression;
|
|
372
|
+
const arg = node.expression.argumentExpression;
|
|
373
|
+
if (T.isIdentifier(obj) && fsNamespaceIdentifiers.has(obj.text)) {
|
|
374
|
+
// Any computed member access on the fs namespace is suspicious;
|
|
375
|
+
// refuse with a clear message.
|
|
376
|
+
const lc = lineCol(node);
|
|
377
|
+
r(file, lc.line, lc.col, `Computed member access on fs (${obj.text}[...]) — refuse, use SafeFsExecutor.`);
|
|
378
|
+
}
|
|
379
|
+
if (T.isIdentifier(obj) && childProcessIdentifiers.has(obj.text)) {
|
|
380
|
+
const lc = lineCol(node);
|
|
381
|
+
r(file, lc.line, lc.col, `Computed member access on child_process (${obj.text}[...]) — refuse, use SafeGitExecutor.`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
T.forEachChild(node, visit);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function firstArgIsGit(call) {
|
|
389
|
+
const args = call.arguments;
|
|
390
|
+
if (args.length === 0) return false;
|
|
391
|
+
const first = args[0];
|
|
392
|
+
const T = ts();
|
|
393
|
+
if (T.isStringLiteral(first)) {
|
|
394
|
+
if (first.text === 'git') return true;
|
|
395
|
+
if (first.text.startsWith('git ')) return true;
|
|
396
|
+
}
|
|
397
|
+
if (T.isTemplateExpression(first) || T.isNoSubstitutionTemplateLiteral(first)) {
|
|
398
|
+
// Best-effort: check the head.
|
|
399
|
+
const text = first.getText(sf);
|
|
400
|
+
if (/^[`'"]git[\s'"]/.test(text)) return true;
|
|
401
|
+
}
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
visit(sf);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── Shell + package.json grep ──────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
const DESTRUCTIVE_GIT_VERBS = [
|
|
411
|
+
'add', 'am', 'apply', 'branch', 'checkout', 'cherry-pick',
|
|
412
|
+
'clean', 'clone', 'commit', 'fetch', 'gc', 'init', 'merge',
|
|
413
|
+
'mv', 'pull', 'push', 'rebase', 'reset', 'restore', 'revert',
|
|
414
|
+
'rm', 'stash', 'submodule', 'switch', 'tag', 'update-ref',
|
|
415
|
+
'worktree', 'prune', 'notes', 'replace', 'filter-branch',
|
|
416
|
+
];
|
|
417
|
+
const SHELL_GIT_REGEX = new RegExp(
|
|
418
|
+
String.raw`(?:^|[\s;&|()])git\s+(?:-C\s+\S+\s+|-c\s+\S+\s+)*(${DESTRUCTIVE_GIT_VERBS.join('|')})\b`,
|
|
419
|
+
'g',
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const SHELL_ALLOWLIST = new Set([
|
|
423
|
+
'scripts/setup-imessage-hardlink.sh',
|
|
424
|
+
// Transitional: pre-existing template script with destructive git verbs.
|
|
425
|
+
// Tracked under commitment://incremental-migration (due 2026-05-03). PR #2
|
|
426
|
+
// ports this to a Node script that uses SafeGitExecutor and removes this
|
|
427
|
+
// entry.
|
|
428
|
+
'src/templates/scripts/git-sync-gate.sh',
|
|
429
|
+
]);
|
|
430
|
+
|
|
431
|
+
function lintShellFile(file, text) {
|
|
432
|
+
const rel = path.relative(ROOT, path.resolve(file));
|
|
433
|
+
if (SHELL_ALLOWLIST.has(rel)) return;
|
|
434
|
+
const lines = text.split('\n');
|
|
435
|
+
for (let i = 0; i < lines.length; i++) {
|
|
436
|
+
const line = lines[i];
|
|
437
|
+
// Skip comments
|
|
438
|
+
const stripped = line.replace(/^\s*#.*$/, '');
|
|
439
|
+
if (!stripped) continue;
|
|
440
|
+
SHELL_GIT_REGEX.lastIndex = 0;
|
|
441
|
+
const m = SHELL_GIT_REGEX.exec(stripped);
|
|
442
|
+
if (m) {
|
|
443
|
+
report(file, i + 1, (m.index || 0) + 1, `Shell script invokes destructive 'git ${m[1]}' — port to a Node script using SafeGitExecutor.`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function lintPackageJsonScripts(file, text) {
|
|
449
|
+
let pkg;
|
|
450
|
+
try {
|
|
451
|
+
pkg = JSON.parse(text);
|
|
452
|
+
} catch {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const scripts = pkg.scripts || {};
|
|
456
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
457
|
+
if (typeof cmd !== 'string') continue;
|
|
458
|
+
SHELL_GIT_REGEX.lastIndex = 0;
|
|
459
|
+
const m = SHELL_GIT_REGEX.exec(cmd);
|
|
460
|
+
if (m) {
|
|
461
|
+
report(file, 1, 1, `npm script "${name}" runs destructive 'git ${m[1]}' — refuse.`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ── File enumeration ───────────────────────────────────────────────
|
|
467
|
+
|
|
468
|
+
const TARGET_DIRS = ['src', 'tests', 'scripts'];
|
|
469
|
+
const TARGET_EXTS = ['.ts', '.tsx', '.js', '.mjs', '.cjs'];
|
|
470
|
+
|
|
471
|
+
function walkDir(dir, out) {
|
|
472
|
+
let entries;
|
|
473
|
+
try {
|
|
474
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
475
|
+
} catch {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
for (const e of entries) {
|
|
479
|
+
if (e.name === 'node_modules' || e.name === '.git' || e.name === 'dist') continue;
|
|
480
|
+
const full = path.join(dir, e.name);
|
|
481
|
+
if (e.isDirectory()) {
|
|
482
|
+
walkDir(full, out);
|
|
483
|
+
} else if (e.isFile()) {
|
|
484
|
+
const ext = path.extname(e.name);
|
|
485
|
+
if (TARGET_EXTS.includes(ext)) out.push(full);
|
|
486
|
+
else if (e.name.endsWith('.sh')) out.push(full);
|
|
487
|
+
else if (e.name === 'package.json') out.push(full);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function gatherFilesFromArgs(args) {
|
|
493
|
+
const out = [];
|
|
494
|
+
for (const a of args) {
|
|
495
|
+
const full = path.resolve(a);
|
|
496
|
+
if (!fs.existsSync(full)) continue;
|
|
497
|
+
const st = fs.statSync(full);
|
|
498
|
+
if (st.isDirectory()) walkDir(full, out);
|
|
499
|
+
else out.push(full);
|
|
500
|
+
}
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function gatherStagedFiles() {
|
|
505
|
+
// safe-git-allow: incremental-migration
|
|
506
|
+
const stdout = execSync('git diff --cached --name-only --diff-filter=ACMR', {
|
|
507
|
+
cwd: ROOT,
|
|
508
|
+
encoding: 'utf8',
|
|
509
|
+
});
|
|
510
|
+
return stdout
|
|
511
|
+
.split('\n')
|
|
512
|
+
.map((s) => s.trim())
|
|
513
|
+
.filter(Boolean)
|
|
514
|
+
.map((rel) => path.resolve(ROOT, rel))
|
|
515
|
+
.filter((full) => {
|
|
516
|
+
try {
|
|
517
|
+
return fs.statSync(full).isFile();
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function gatherAll() {
|
|
525
|
+
const out = [];
|
|
526
|
+
for (const d of TARGET_DIRS) {
|
|
527
|
+
walkDir(path.join(ROOT, d), out);
|
|
528
|
+
}
|
|
529
|
+
// Also include package.json at root.
|
|
530
|
+
out.push(path.join(ROOT, 'package.json'));
|
|
531
|
+
return out;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Main ───────────────────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
function main() {
|
|
537
|
+
const argv = process.argv.slice(2);
|
|
538
|
+
let files;
|
|
539
|
+
if (argv.includes('--staged')) {
|
|
540
|
+
files = gatherStagedFiles();
|
|
541
|
+
} else if (argv.length > 0) {
|
|
542
|
+
files = gatherFilesFromArgs(argv);
|
|
543
|
+
} else {
|
|
544
|
+
files = gatherAll();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
for (const file of files) {
|
|
548
|
+
let text;
|
|
549
|
+
try {
|
|
550
|
+
text = fs.readFileSync(file, 'utf8');
|
|
551
|
+
} catch {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const rel = path.relative(ROOT, file);
|
|
556
|
+
// package.json scripts grep
|
|
557
|
+
if (path.basename(file) === 'package.json' && rel === 'package.json') {
|
|
558
|
+
lintPackageJsonScripts(file, text);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
// Shell grep
|
|
562
|
+
if (file.endsWith('.sh')) {
|
|
563
|
+
lintShellFile(file, text);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
// Allowlist check (closed)
|
|
567
|
+
if (ALLOWLIST.has(rel)) continue;
|
|
568
|
+
if (hasAllowComment(text)) continue;
|
|
569
|
+
|
|
570
|
+
// AST lint for ts/js/mjs/cjs
|
|
571
|
+
try {
|
|
572
|
+
lintTsFile(file, text);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
// Parse failure → emit a soft warning, not a violation.
|
|
575
|
+
process.stderr.write(`[lint-no-direct-destructive] failed to parse ${rel}: ${err.message}\n`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (violations.length === 0) {
|
|
580
|
+
return 0;
|
|
581
|
+
}
|
|
582
|
+
process.stderr.write('\n');
|
|
583
|
+
process.stderr.write('╔════════════════════════════════════════════════════════════════════╗\n');
|
|
584
|
+
process.stderr.write('║ lint-no-direct-destructive — VIOLATIONS ║\n');
|
|
585
|
+
process.stderr.write('╚════════════════════════════════════════════════════════════════════╝\n');
|
|
586
|
+
process.stderr.write('\n');
|
|
587
|
+
for (const v of violations) {
|
|
588
|
+
const rel = path.relative(ROOT, v.file);
|
|
589
|
+
process.stderr.write(` ${rel}:${v.line}:${v.col}\n ${v.msg}\n\n`);
|
|
590
|
+
}
|
|
591
|
+
process.stderr.write(`Total: ${violations.length} violation(s).\n`);
|
|
592
|
+
process.stderr.write('See docs/specs/COMPREHENSIVE-DESTRUCTIVE-TOOL-CONTAINMENT-SPEC.md for guidance.\n');
|
|
593
|
+
return 1;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const code = main();
|
|
597
|
+
process.exit(code);
|
|
@@ -54,6 +54,7 @@ function generateKeypair() {
|
|
|
54
54
|
function verifyStash() {
|
|
55
55
|
let stashList;
|
|
56
56
|
try {
|
|
57
|
+
// safe-git-allow: incremental-migration
|
|
57
58
|
stashList = execSync('git stash list', { encoding: 'utf-8' });
|
|
58
59
|
} catch (err) {
|
|
59
60
|
logStep(`No stash present (or not a git repo). Skipping stash verification.`);
|
package/scripts/pre-push-gate.js
CHANGED
|
@@ -271,6 +271,30 @@ if (!process.env.CI) {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
// ── Destructive-tool containment lint (full repo) ─────────────────────
|
|
275
|
+
//
|
|
276
|
+
// Runs the lint-no-direct-destructive AST scanner across the whole repo on
|
|
277
|
+
// every push. Pre-commit only runs it over staged files; pre-push catches
|
|
278
|
+
// commits that landed before the rule existed (or before the marker scheme
|
|
279
|
+
// expired). Fails the push on any violation.
|
|
280
|
+
//
|
|
281
|
+
// Wired here rather than in .husky/pre-push because the husky hook files
|
|
282
|
+
// are managed by a sandboxed flow that this gate can extend.
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const { spawnSync } = await import('node:child_process');
|
|
286
|
+
const result = spawnSync(
|
|
287
|
+
process.execPath,
|
|
288
|
+
[path.join(ROOT, 'scripts/lint-no-direct-destructive.js')],
|
|
289
|
+
{ cwd: ROOT, stdio: ['ignore', 'inherit', 'inherit'] },
|
|
290
|
+
);
|
|
291
|
+
if (result.status !== 0) {
|
|
292
|
+
errors.push('lint-no-direct-destructive: violations detected (see output above)');
|
|
293
|
+
}
|
|
294
|
+
} catch (err) {
|
|
295
|
+
warnings.push(`lint-no-direct-destructive failed to run: ${err.message}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
274
298
|
// ── Report ────────────────────────────────────────────────────────────
|
|
275
299
|
|
|
276
300
|
if (errors.length > 0 || warnings.length > 0) {
|
|
@@ -38,11 +38,13 @@ function findSessionContext(startCwd) {
|
|
|
38
38
|
|
|
39
39
|
function gitParents(cwd) {
|
|
40
40
|
// Determine if this is a merge: presence of $GIT_REFLOG_ACTION=merge OR MERGE_HEAD file
|
|
41
|
+
// safe-git-allow: incremental-migration
|
|
41
42
|
const gitDir = execFileSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], { encoding: 'utf-8' }).trim();
|
|
42
43
|
const mergeHeadPath = path.join(cwd, gitDir, 'MERGE_HEAD');
|
|
43
44
|
let parents = [];
|
|
44
45
|
// Primary parent = current HEAD (or 0...0 for initial commit)
|
|
45
46
|
try {
|
|
47
|
+
// safe-git-allow: incremental-migration
|
|
46
48
|
const head = execFileSync('git', ['-C', cwd, 'rev-parse', 'HEAD'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
47
49
|
parents.push(head);
|
|
48
50
|
} catch {
|
|
@@ -58,6 +60,7 @@ function gitParents(cwd) {
|
|
|
58
60
|
function gitWriteTree(cwd) {
|
|
59
61
|
// K-fix: honor $GIT_INDEX_FILE (set by `git commit -a` and `git commit <file>`)
|
|
60
62
|
const env = { ...process.env };
|
|
63
|
+
// safe-git-allow: incremental-migration
|
|
61
64
|
return execFileSync('git', ['-C', cwd, 'write-tree', '--missing-ok'], {
|
|
62
65
|
encoding: 'utf-8', env,
|
|
63
66
|
}).trim();
|
|
@@ -151,6 +154,7 @@ async function main() {
|
|
|
151
154
|
trailerArgs.push('--trailer', t);
|
|
152
155
|
}
|
|
153
156
|
try {
|
|
157
|
+
// safe-git-allow: incremental-migration
|
|
154
158
|
execFileSync('git', ['interpret-trailers', '--in-place', ...trailerArgs, commitMsgFile], {
|
|
155
159
|
stdio: 'inherit',
|
|
156
160
|
});
|
|
@@ -94,6 +94,7 @@ async function main() {
|
|
|
94
94
|
// Collect staged files (from `git diff --cached --name-only`)
|
|
95
95
|
let stagedFiles = [];
|
|
96
96
|
try {
|
|
97
|
+
// safe-git-allow: incremental-migration
|
|
97
98
|
stagedFiles = execSync('git diff --cached --name-only -z', { encoding: 'utf-8', cwd })
|
|
98
99
|
.split('\0').filter(Boolean);
|
|
99
100
|
} catch { /* @silent-fallback-ok */ }
|