Skip to content

Commit 9595c29

Browse files
authored
refactor: migrate maintained runtime leaves to TypeScript (#989)
## Summary - migrate nine maintained CommonJS runtime modules to strict TypeScript across runtime boundaries, task input handling, CLI formatting, buffering/report formatting, and attach socket paths - preserve emitted CommonJS module keys, function arities, runtime paths, socket permissions, and package contents - keep each concern in its own reviewable commit while shipping the stack as one PR ## Migrated modules - `src/ledger-sequence.ts` - `src/task-startup-error.ts` - `src/input-helpers.ts` - `src/task-runner.ts` - `cli/event-copy.ts` - `cli/message-formatter-utils.ts` - `src/message-buffer.ts` - `src/template-validation/report-formatter.ts` - `src/attach/socket-paths.ts` ## Validation - `npm run typecheck` - `npm run lint -- --quiet` - focused unit/integration suites for every migrated surface - built-package runtime smoke test - public export key and function-arity parity checks where applicable - Opcore graph update, repository check, changed/staged checks, Sense, and introduced-change validation ## Scope These are maintained Node runtime surfaces. Current native-v2 roadmap issue #986 explicitly defers attach/reconnect and does not replace these modules in its near-term foreground-only scope. No behavior or feature changes are intended.
1 parent 673f5b7 commit 9595c29

16 files changed

Lines changed: 437 additions & 299 deletions

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ report/
5858
.turbo/
5959
*.cast
6060
# Generated/temp files
61+
cli/event-copy.js
62+
cli/message-formatter-utils.js
6163
test-metadata-manual.sh
6264
test-isolated-fix.js
6365
lib/agent-cli-provider/
@@ -138,6 +140,11 @@ lib/settings-validation.d.ts
138140
lib/settings-issue-providers.js
139141
lib/settings-issue-providers.d.ts
140142
src/guidance-topics.js
143+
src/input-helpers.js
144+
src/ledger-sequence.js
145+
src/message-buffer.js
146+
src/task-startup-error.js
147+
src/task-runner.js
141148
src/omp-blob-root.js
142149
src/omp-config-overlay.js
143150
task-lib/completion.js
@@ -152,8 +159,10 @@ src/agent/critical-agent-policy.js
152159
src/agent/provider-control-plane.js
153160
src/agent/structured-output-error.js
154161
src/agent/validation-platform.js
162+
src/attach/socket-paths.js
155163
src/providers/anthropic/index.js
156164
src/providers/capabilities.js
157165
src/providers/google/index.js
158166
src/providers/openai/index.js
159167
src/providers/opencode/index.js
168+
src/template-validation/report-formatter.js
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
const EVENT_COPY = {
22
IMPLEMENTATION_READY: 'Implementation ready',
33
PR_CREATED: 'Pull request created',
4-
};
4+
} as const;
55

6-
function formatMergeStatus(merged) {
6+
function formatMergeStatus(merged: unknown): string | null {
77
if (merged === true || merged === 'true') return 'merged';
88
if (merged === false || merged === 'false') return 'auto-merge pending approval';
99
return null;
1010
}
1111

12-
module.exports = { EVENT_COPY, formatMergeStatus };
12+
export = { EVENT_COPY, formatMergeStatus };

cli/message-formatter-utils.js

Lines changed: 0 additions & 75 deletions
This file was deleted.

cli/message-formatter-utils.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import chalk = require('chalk');
2+
3+
interface MessagePrefixData {
4+
sender: string;
5+
cluster_id?: string | null;
6+
sender_model?: string | null;
7+
}
8+
9+
/**
10+
* Get color for sender based on consistent hashing.
11+
*/
12+
function getColorForSender(sender: string): chalk.Chalk {
13+
const colors = [chalk.cyan, chalk.magenta, chalk.yellow, chalk.green, chalk.blue];
14+
let hash = 0;
15+
for (let i = 0; i < sender.length; i++) {
16+
hash = (hash << 5) - hash + sender.charCodeAt(i);
17+
hash = hash & hash;
18+
}
19+
return colors[Math.abs(hash) % colors.length] ?? chalk.cyan;
20+
}
21+
22+
/**
23+
* Build message prefix with timestamp, sender, and optional cluster ID.
24+
*/
25+
function buildMessagePrefix(
26+
msg: MessagePrefixData,
27+
showClusterId: boolean,
28+
isActive: boolean
29+
): string {
30+
const color = isActive ? getColorForSender(msg.sender) : chalk.dim;
31+
32+
let senderLabel = msg.sender;
33+
if (showClusterId && msg.cluster_id) {
34+
senderLabel = `${msg.cluster_id}/${msg.sender}`;
35+
}
36+
37+
const modelSuffix = msg.sender_model ? chalk.dim(` [${msg.sender_model}]`) : '';
38+
return color(`${senderLabel.padEnd(showClusterId ? 25 : 15)} |`) + modelSuffix;
39+
}
40+
41+
/**
42+
* Build cluster prefix for watch mode.
43+
*/
44+
function buildClusterPrefix(clusterId: string, isActive: boolean): string {
45+
const color = isActive ? chalk.white : chalk.dim;
46+
return color(`${clusterId.padEnd(20)} |`);
47+
}
48+
49+
/**
50+
* Parse and normalize data fields (handles string JSON).
51+
*/
52+
function parseDataField(data: unknown): unknown {
53+
if (typeof data === 'string') {
54+
try {
55+
const parsed: unknown = JSON.parse(data);
56+
return parsed;
57+
} catch {
58+
return [];
59+
}
60+
}
61+
return Array.isArray(data) ? data : [];
62+
}
63+
64+
export = {
65+
getColorForSender,
66+
buildMessagePrefix,
67+
buildClusterPrefix,
68+
parseDataField,
69+
};

eslint.config.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@ export default [
355355
'lib/cluster/**',
356356
'lib/hosted-session/**',
357357
'lib/target/**',
358+
'cli/event-copy.js',
359+
'cli/message-formatter-utils.js',
358360
'lib/clusters-registry.js',
359361
'lib/completion.js',
360362
'lib/compose-utils.js',
@@ -392,6 +394,11 @@ export default [
392394
'lib/start-cluster.js',
393395
'lib/stream-json-parser.js',
394396
'src/guidance-topics.js',
397+
'src/input-helpers.js',
398+
'src/ledger-sequence.js',
399+
'src/message-buffer.js',
400+
'src/task-startup-error.js',
401+
'src/task-runner.js',
395402
'src/omp-blob-root.js',
396403
'src/omp-config-overlay.js',
397404
'task-lib/completion.js',
@@ -406,11 +413,13 @@ export default [
406413
'src/agent/provider-control-plane.js',
407414
'src/agent/structured-output-error.js',
408415
'src/agent/validation-platform.js',
416+
'src/attach/socket-paths.js',
409417
'src/providers/anthropic/index.js',
410418
'src/providers/capabilities.js',
411419
'src/providers/google/index.js',
412420
'src/providers/openai/index.js',
413421
'src/providers/opencode/index.js',
422+
'src/template-validation/report-formatter.js',
414423
],
415424
},
416425
prettierConfig,
Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,34 @@
1-
/**
2-
* Deterministic attach socket paths.
3-
*
4-
* Unix-domain sockets have a small path budget (104 bytes on macOS). Keep the
5-
* live socket namespace independent from HOME while retaining one namespace
6-
* per OS user and Zeroshot home.
7-
*/
8-
9-
const crypto = require('crypto');
10-
const fs = require('fs');
11-
const os = require('os');
12-
const path = require('path');
1+
import crypto = require('crypto');
2+
import fs = require('fs');
3+
import os = require('os');
4+
import path = require('path');
135

146
const SOCKET_ROOT = process.platform === 'win32' ? null : '/tmp';
157
const SOCKET_DIR_MODE = 0o700;
168

17-
function shortHash(value) {
9+
function shortHash(value: string): string {
1810
return crypto.createHash('sha256').update(value).digest('hex').slice(0, 16);
1911
}
2012

21-
function userNamespace() {
13+
function userNamespace(): string {
2214
if (typeof process.getuid === 'function') {
2315
return String(process.getuid());
2416
}
2517
return shortHash(os.userInfo().username);
2618
}
2719

28-
function resolveHomeDir(env = process.env) {
20+
function resolveHomeDir(env: NodeJS.ProcessEnv = process.env): string {
2921
return env.ZEROSHOT_HOME || env.HOME || env.USERPROFILE || os.homedir();
3022
}
3123

32-
function getSocketDir(homeDir = resolveHomeDir()) {
33-
if (process.platform === 'win32') {
24+
function getSocketDir(homeDir = resolveHomeDir()): string {
25+
if (SOCKET_ROOT === null) {
3426
return path.join(homeDir, '.zeroshot', 'sockets');
3527
}
3628
return path.join(SOCKET_ROOT, `zeroshot-${userNamespace()}-${shortHash(homeDir)}`);
3729
}
3830

39-
function assertSafeSocketDir(socketDir) {
31+
function assertSafeSocketDir(socketDir: string): void {
4032
const stat = fs.lstatSync(socketDir);
4133
if (!stat.isDirectory() || stat.isSymbolicLink()) {
4234
throw new Error(`Attach socket path is not a directory: ${socketDir}`);
@@ -46,7 +38,7 @@ function assertSafeSocketDir(socketDir) {
4638
}
4739
}
4840

49-
function ensureOwnedDirectory(socketDir) {
41+
function ensureOwnedDirectory(socketDir: string): string {
5042
fs.mkdirSync(socketDir, { recursive: true, mode: SOCKET_DIR_MODE });
5143
assertSafeSocketDir(socketDir);
5244
if (process.platform !== 'win32') {
@@ -55,26 +47,30 @@ function ensureOwnedDirectory(socketDir) {
5547
return socketDir;
5648
}
5749

58-
function ensureSocketDir(homeDir = resolveHomeDir()) {
50+
function ensureSocketDir(homeDir = resolveHomeDir()): string {
5951
const socketDir = getSocketDir(homeDir);
6052
return ensureOwnedDirectory(socketDir);
6153
}
6254

63-
function getTaskSocketPath(taskId, homeDir = resolveHomeDir()) {
55+
function getTaskSocketPath(taskId: string, homeDir = resolveHomeDir()): string {
6456
return path.join(ensureSocketDir(homeDir), `${taskId}.sock`);
6557
}
6658

67-
function getAgentSocketPath(clusterId, agentId, homeDir = resolveHomeDir()) {
59+
function getAgentSocketPath(
60+
clusterId: string,
61+
agentId: string,
62+
homeDir = resolveHomeDir()
63+
): string {
6864
const clusterDir = path.join(ensureSocketDir(homeDir), clusterId);
6965
ensureOwnedDirectory(clusterDir);
7066
return path.join(clusterDir, `${agentId}.sock`);
7167
}
7268

73-
function getClusterSocketPath(clusterId, homeDir = resolveHomeDir()) {
69+
function getClusterSocketPath(clusterId: string, homeDir = resolveHomeDir()): string {
7470
return path.join(ensureSocketDir(homeDir), `${clusterId}.sock`);
7571
}
7672

77-
module.exports = {
73+
export = {
7874
SOCKET_DIR_MODE,
7975
resolveHomeDir,
8076
getSocketDir,
Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1+
import fs = require('fs');
2+
import path = require('path');
3+
4+
interface ManualInput {
5+
number: null;
6+
title: string;
7+
body: string;
8+
labels: unknown[];
9+
comments: unknown[];
10+
url: null;
11+
context: string;
12+
}
13+
114
/**
215
* Input Helpers - Create input data from text or files
316
*
417
* Provides fallback input methods for non-issue-based input:
518
* - Plain text input
619
* - File input (markdown)
720
*/
8-
9-
const fs = require('fs');
10-
const path = require('path');
11-
1221
class InputHelpers {
13-
/**
14-
* Create a plain text input wrapper
15-
* @param {String} text - Plain text input
16-
* @returns {Object} Structured context
17-
*/
18-
static createTextInput(text) {
22+
static createTextInput(text: string): ManualInput {
1923
return {
2024
number: null,
2125
title: 'Manual Input',
@@ -27,26 +31,15 @@ class InputHelpers {
2731
};
2832
}
2933

30-
/**
31-
* Create input from markdown file
32-
* @param {String} filePath - Path to markdown file (.md or .markdown)
33-
* @returns {Object} Structured context matching issue format
34-
*/
35-
static createFileInput(filePath) {
36-
// Resolve relative paths
34+
static createFileInput(filePath: string): ManualInput {
3735
const resolvedPath = path.resolve(filePath);
38-
39-
// Validate file exists
4036
if (!fs.existsSync(resolvedPath)) {
4137
throw new Error(`File not found: ${filePath}`);
4238
}
4339

44-
// Read file content
4540
const fileContent = fs.readFileSync(resolvedPath, 'utf8');
46-
47-
// Extract title from first header or use filename
48-
const headerMatch = fileContent.match(/^#\s+(.+)$/m);
49-
const extractedTitle = headerMatch ? headerMatch[1].trim() : null;
41+
const headerMatch = /^#\s+(.+)$/m.exec(fileContent);
42+
const extractedTitle = headerMatch?.[1]?.trim() ?? null;
5043
const fallbackTitle = path.basename(filePath, path.extname(filePath));
5144
const title = extractedTitle || fallbackTitle;
5245

@@ -62,4 +55,4 @@ class InputHelpers {
6255
}
6356
}
6457

65-
module.exports = InputHelpers;
58+
export = InputHelpers;

0 commit comments

Comments
 (0)