zerg-ztc 0.1.4 → 0.1.6

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.
Files changed (139) hide show
  1. package/dist/App.d.ts.map +1 -1
  2. package/dist/App.js +141 -16
  3. package/dist/App.js.map +1 -1
  4. package/dist/agent/agent.d.ts +4 -0
  5. package/dist/agent/agent.d.ts.map +1 -1
  6. package/dist/agent/agent.js +21 -3
  7. package/dist/agent/agent.js.map +1 -1
  8. package/dist/agent/commands/config.d.ts.map +1 -1
  9. package/dist/agent/commands/config.js +68 -2
  10. package/dist/agent/commands/config.js.map +1 -1
  11. package/dist/agent/commands/index.d.ts.map +1 -1
  12. package/dist/agent/commands/index.js +2 -1
  13. package/dist/agent/commands/index.js.map +1 -1
  14. package/dist/agent/commands/update.d.ts +3 -0
  15. package/dist/agent/commands/update.d.ts.map +1 -0
  16. package/dist/agent/commands/update.js +33 -0
  17. package/dist/agent/commands/update.js.map +1 -0
  18. package/dist/agent/tools/file.d.ts.map +1 -1
  19. package/dist/agent/tools/file.js +10 -6
  20. package/dist/agent/tools/file.js.map +1 -1
  21. package/dist/agent/tools/index.d.ts +2 -2
  22. package/dist/agent/tools/index.d.ts.map +1 -1
  23. package/dist/agent/tools/index.js +2 -2
  24. package/dist/agent/tools/index.js.map +1 -1
  25. package/dist/agent/tools/search.d.ts.map +1 -1
  26. package/dist/agent/tools/search.js +5 -4
  27. package/dist/agent/tools/search.js.map +1 -1
  28. package/dist/agent/tools/shell.d.ts.map +1 -1
  29. package/dist/agent/tools/shell.js +7 -3
  30. package/dist/agent/tools/shell.js.map +1 -1
  31. package/dist/agent/tools/types.d.ts +4 -1
  32. package/dist/agent/tools/types.d.ts.map +1 -1
  33. package/dist/cli.js +46 -31
  34. package/dist/cli.js.map +1 -1
  35. package/dist/components/ActivityLine.d.ts +11 -0
  36. package/dist/components/ActivityLine.d.ts.map +1 -0
  37. package/dist/components/ActivityLine.js +9 -0
  38. package/dist/components/ActivityLine.js.map +1 -0
  39. package/dist/components/FullScreen.d.ts +1 -0
  40. package/dist/components/FullScreen.d.ts.map +1 -1
  41. package/dist/components/FullScreen.js +2 -2
  42. package/dist/components/FullScreen.js.map +1 -1
  43. package/dist/components/MessageList.d.ts +2 -1
  44. package/dist/components/MessageList.d.ts.map +1 -1
  45. package/dist/components/MessageList.js +41 -2
  46. package/dist/components/MessageList.js.map +1 -1
  47. package/dist/components/SingleMessage.d.ts +9 -0
  48. package/dist/components/SingleMessage.d.ts.map +1 -0
  49. package/dist/components/SingleMessage.js +27 -0
  50. package/dist/components/SingleMessage.js.map +1 -0
  51. package/dist/components/StatusBar.d.ts +1 -0
  52. package/dist/components/StatusBar.d.ts.map +1 -1
  53. package/dist/components/StatusBar.js +2 -1
  54. package/dist/components/StatusBar.js.map +1 -1
  55. package/dist/components/index.d.ts +2 -0
  56. package/dist/components/index.d.ts.map +1 -1
  57. package/dist/components/index.js +2 -0
  58. package/dist/components/index.js.map +1 -1
  59. package/dist/config/types.d.ts +1 -0
  60. package/dist/config/types.d.ts.map +1 -1
  61. package/dist/config.d.ts.map +1 -1
  62. package/dist/config.js +8 -0
  63. package/dist/config.js.map +1 -1
  64. package/dist/ui/views/activity_line.d.ts +11 -0
  65. package/dist/ui/views/activity_line.d.ts.map +1 -0
  66. package/dist/ui/views/activity_line.js +20 -0
  67. package/dist/ui/views/activity_line.js.map +1 -0
  68. package/dist/ui/views/app.d.ts +5 -2
  69. package/dist/ui/views/app.d.ts.map +1 -1
  70. package/dist/ui/views/app.js +17 -14
  71. package/dist/ui/views/app.js.map +1 -1
  72. package/dist/ui/views/header.d.ts.map +1 -1
  73. package/dist/ui/views/header.js +3 -4
  74. package/dist/ui/views/header.js.map +1 -1
  75. package/dist/ui/views/input_area.d.ts.map +1 -1
  76. package/dist/ui/views/input_area.js +25 -12
  77. package/dist/ui/views/input_area.js.map +1 -1
  78. package/dist/ui/views/message_list.d.ts +3 -2
  79. package/dist/ui/views/message_list.d.ts.map +1 -1
  80. package/dist/ui/views/message_list.js +33 -19
  81. package/dist/ui/views/message_list.js.map +1 -1
  82. package/dist/ui/views/status_bar.d.ts +2 -1
  83. package/dist/ui/views/status_bar.d.ts.map +1 -1
  84. package/dist/ui/views/status_bar.js +4 -2
  85. package/dist/ui/views/status_bar.js.map +1 -1
  86. package/dist/utils/shell.d.ts.map +1 -1
  87. package/dist/utils/shell.js +9 -1
  88. package/dist/utils/shell.js.map +1 -1
  89. package/dist/utils/spinner_frames.d.ts +2 -0
  90. package/dist/utils/spinner_frames.d.ts.map +1 -0
  91. package/dist/utils/spinner_frames.js +2 -0
  92. package/dist/utils/spinner_frames.js.map +1 -0
  93. package/dist/utils/spinner_verbs.d.ts +4 -0
  94. package/dist/utils/spinner_verbs.d.ts.map +1 -0
  95. package/dist/utils/spinner_verbs.js +22 -0
  96. package/dist/utils/spinner_verbs.js.map +1 -0
  97. package/dist/utils/tool_trace.d.ts.map +1 -1
  98. package/dist/utils/tool_trace.js +12 -2
  99. package/dist/utils/tool_trace.js.map +1 -1
  100. package/dist/utils/update.d.ts +9 -0
  101. package/dist/utils/update.d.ts.map +1 -0
  102. package/dist/utils/update.js +37 -0
  103. package/dist/utils/update.js.map +1 -0
  104. package/dist/utils/version.d.ts +2 -0
  105. package/dist/utils/version.d.ts.map +1 -0
  106. package/dist/utils/version.js +16 -0
  107. package/dist/utils/version.js.map +1 -0
  108. package/package.json +1 -1
  109. package/src/App.tsx +180 -26
  110. package/src/agent/agent.ts +26 -6
  111. package/src/agent/commands/config.ts +76 -2
  112. package/src/agent/commands/index.ts +2 -0
  113. package/src/agent/commands/update.ts +32 -0
  114. package/src/agent/tools/file.ts +24 -19
  115. package/src/agent/tools/index.ts +5 -4
  116. package/src/agent/tools/search.ts +6 -5
  117. package/src/agent/tools/shell.ts +13 -9
  118. package/src/agent/tools/types.ts +5 -1
  119. package/src/cli.tsx +50 -30
  120. package/src/components/ActivityLine.tsx +23 -0
  121. package/src/components/FullScreen.tsx +4 -3
  122. package/src/components/MessageList.tsx +52 -6
  123. package/src/components/SingleMessage.tsx +59 -0
  124. package/src/components/StatusBar.tsx +3 -0
  125. package/src/components/index.tsx +3 -1
  126. package/src/config/types.ts +1 -0
  127. package/src/config.ts +8 -0
  128. package/src/ui/views/activity_line.ts +33 -0
  129. package/src/ui/views/app.ts +23 -14
  130. package/src/ui/views/header.ts +3 -4
  131. package/src/ui/views/input_area.ts +28 -17
  132. package/src/ui/views/message_list.ts +36 -20
  133. package/src/ui/views/status_bar.ts +5 -1
  134. package/src/utils/shell.ts +10 -1
  135. package/src/utils/spinner_frames.ts +1 -0
  136. package/src/utils/spinner_verbs.ts +23 -0
  137. package/src/utils/tool_trace.ts +12 -2
  138. package/src/utils/update.ts +44 -0
  139. package/src/utils/version.ts +15 -0
@@ -5,6 +5,7 @@ import { buildHeaderView } from './header.js';
5
5
  import { buildMessageListView } from './message_list.js';
6
6
  import { buildInputAreaView, estimateInputLines } from './input_area.js';
7
7
  import { buildStatusBarView } from './status_bar.js';
8
+ import { buildActivityLineView } from './activity_line.js';
8
9
 
9
10
  interface AppViewProps {
10
11
  messages: Message[];
@@ -15,13 +16,16 @@ interface AppViewProps {
15
16
  rows: number;
16
17
  commands: Array<{ name: string; description: string; usage?: string }>;
17
18
  hasApiKey: boolean;
19
+ version?: string;
18
20
  contextLength?: number;
19
21
  contextEstimated?: boolean;
20
22
  provider?: string;
21
23
  model?: string;
22
24
  emulationId?: string;
23
- inputMode?: 'queue' | 'interrupt';
24
25
  toast?: string | null;
26
+ spinnerLabel?: string | null;
27
+ spinnerFrame?: string | null;
28
+ inputMode?: 'queue' | 'interrupt';
25
29
  debug?: boolean;
26
30
  expandToolOutputs?: boolean;
27
31
  }
@@ -48,6 +52,7 @@ export function buildAppView({
48
52
  rows,
49
53
  commands,
50
54
  hasApiKey,
55
+ version = '0.1.0',
51
56
  contextLength,
52
57
  contextEstimated,
53
58
  provider,
@@ -55,17 +60,20 @@ export function buildAppView({
55
60
  emulationId,
56
61
  inputMode,
57
62
  toast,
63
+ spinnerLabel,
64
+ spinnerFrame,
58
65
  debug = false,
59
66
  expandToolOutputs = false
60
67
  }: AppViewProps): LayoutNode {
61
68
  const suggestionLines = 4;
62
- const headerHeight = 6; // logo row + meta row + padding
69
+ const headerHeight = 1;
63
70
  const statusHeight = 1;
71
+ const activityHeight = (agentState.status === 'thinking' || agentState.status === 'streaming') && spinnerLabel ? 3 : 0;
64
72
  const inputLineCount = estimateInputLines(inputState.segments, cols);
65
73
  const previewLines = estimateBadgePreviewLines(inputState);
66
- const maxInputLines = Math.max(1, rows - (headerHeight + statusHeight + suggestionLines + 5));
74
+ const maxInputLines = Math.max(1, rows - (headerHeight + statusHeight + activityHeight + suggestionLines + 5));
67
75
  const inputHeight = Math.min(Math.max(1, inputLineCount + previewLines), maxInputLines) + suggestionLines;
68
- const contentHeight = Math.max(rows - (headerHeight + inputHeight + statusHeight), 5);
76
+ const contentHeight = Math.max(rows - (headerHeight + inputHeight + statusHeight + activityHeight), 5);
69
77
 
70
78
  const placeholder = !hasApiKey ? 'Set API key with /config key <key>' :
71
79
  agentState.status === 'thinking' ? 'Thinking...' :
@@ -74,12 +82,21 @@ export function buildAppView({
74
82
  'Type a message or /help for commands...';
75
83
 
76
84
  return box([
77
- buildHeaderView({ version: '0.1.0', debug }),
85
+ buildHeaderView({ version, debug }),
78
86
  buildMessageListView({ messages, height: contentHeight, debug, expandToolOutputs }),
87
+ buildActivityLineView({ state: agentState, spinnerLabel, spinnerFrame, inputMode }),
88
+ buildInputAreaView({
89
+ state: inputState,
90
+ placeholder,
91
+ disabled: agentState.status !== 'idle' && agentState.status !== 'error',
92
+ commands,
93
+ cols,
94
+ debug
95
+ }),
79
96
  buildStatusBarView({
80
97
  state: agentState,
81
98
  sessionId,
82
- version: '0.1.0',
99
+ version,
83
100
  connectionStatus: hasApiKey ? 'connected' : 'disconnected',
84
101
  contextLength,
85
102
  contextEstimated,
@@ -89,14 +106,6 @@ export function buildAppView({
89
106
  inputMode,
90
107
  toast,
91
108
  debug
92
- }),
93
- buildInputAreaView({
94
- state: inputState,
95
- placeholder,
96
- disabled: agentState.status !== 'idle' && agentState.status !== 'error',
97
- commands,
98
- cols,
99
- debug
100
109
  })
101
110
  ], { flexDirection: 'column' });
102
111
  }
@@ -15,7 +15,6 @@ export function buildHeaderView({
15
15
  showHelp = true,
16
16
  debug = false
17
17
  }: HeaderProps): LayoutNode {
18
- // Single-row header to minimize vertical space
19
18
  return box([
20
19
  box([
21
20
  text(title, { bold: true }),
@@ -37,9 +36,9 @@ export function buildHeaderView({
37
36
  flexDirection: 'row',
38
37
  justifyContent: 'space-between',
39
38
  paddingX: 1,
40
- height: 1,
41
39
  flexShrink: 0,
42
- borderStyle: debug ? 'single' : undefined,
43
- borderColor: debug ? 'cyan' : undefined
40
+ borderStyle: 'single',
41
+ borderColor: debug ? 'cyan' : 'gray',
42
+ marginBottom: 1
44
43
  });
45
44
  }
@@ -216,19 +216,20 @@ export function buildInputAreaView({
216
216
  borderColor: debug ? (disabled ? 'gray' : 'blue') : undefined
217
217
  });
218
218
 
219
- const suggestions = box(
220
- Array.from({ length: suggestionLines }).map((_, index) => {
221
- const cmd = commandMatches[index];
222
- if (!cmd) return text(' ');
223
- const usage = cmd.usage ? ` ${truncate(cmd.usage, 36)}` : '';
224
- return box([
225
- text(`/${cmd.name}`, { color: 'cyan', bold: true }),
226
- text(usage, { color: 'white' }),
227
- text(` — ${truncate(cmd.description, 48)}`, { color: 'gray', dimColor: true })
228
- ], { flexDirection: 'row' });
229
- }),
230
- { flexDirection: 'column', paddingX: 2 }
231
- );
219
+ // Only render suggestion lines if there are actual command matches
220
+ const suggestions = commandMatches.length > 0
221
+ ? box(
222
+ commandMatches.map((cmd) => {
223
+ const usage = cmd.usage ? ` ${truncate(cmd.usage, 36)}` : '';
224
+ return box([
225
+ text(`/${cmd.name}`, { color: 'cyan', bold: true }),
226
+ text(usage, { color: 'white' }),
227
+ text(` — ${truncate(cmd.description, 48)}`, { color: 'gray', dimColor: true })
228
+ ], { flexDirection: 'row' });
229
+ }),
230
+ { flexDirection: 'column', paddingX: 2 }
231
+ )
232
+ : null;
232
233
 
233
234
  const previewLines = showBadgePreview && badgePreview && badgePreview.length > 0
234
235
  ? box(badgePreview.map(line => text(line, { color: 'gray', dimColor: true })), {
@@ -237,8 +238,18 @@ export function buildInputAreaView({
237
238
  })
238
239
  : null;
239
240
 
240
- return box(
241
- previewLines ? [inputLine, previewLines, suggestions] : [inputLine, suggestions],
242
- { flexDirection: 'column', flexShrink: 0 }
243
- );
241
+ // Horizontal separator lines
242
+ const separatorTop = text('─'.repeat(Math.max(10, cols - 2)), { color: 'gray', dimColor: true });
243
+ const separatorBottom = text('─'.repeat(Math.max(10, cols - 2)), { color: 'gray', dimColor: true });
244
+
245
+ // Build the content array, filtering out nulls
246
+ const content: LayoutNode[] = [separatorTop, inputLine];
247
+ if (previewLines) content.push(previewLines);
248
+ if (suggestions) content.push(suggestions);
249
+ content.push(separatorBottom);
250
+
251
+ return box(content, {
252
+ flexDirection: 'column',
253
+ flexShrink: 0
254
+ });
244
255
  }
@@ -278,7 +278,17 @@ function renderTable(lines: string[]): LayoutNode[] {
278
278
 
279
279
  function renderMarkdownLines(lines: string[]): LayoutNode[] {
280
280
  const normalized: string[] = [];
281
+ let inCodeBlock = false;
281
282
  lines.forEach(line => {
283
+ if (line.trim().startsWith('```')) {
284
+ inCodeBlock = !inCodeBlock;
285
+ normalized.push(line);
286
+ return;
287
+ }
288
+ if (inCodeBlock) {
289
+ normalized.push(line);
290
+ return;
291
+ }
282
292
  if (line.includes('||') && line.includes('|')) {
283
293
  const segments = line.split('||').map(segment => segment.trim()).filter(Boolean);
284
294
  segments.forEach(segment => {
@@ -288,6 +298,7 @@ function renderMarkdownLines(lines: string[]): LayoutNode[] {
288
298
  normalized.push(out);
289
299
  });
290
300
  } else {
301
+ // Preserve the original line - don't collapse whitespace as it breaks formatting
291
302
  normalized.push(line);
292
303
  }
293
304
  });
@@ -373,10 +384,11 @@ function clipMessage(message: Message, available: number): Message | null {
373
384
 
374
385
  interface MessageListProps {
375
386
  messages: Message[];
376
- height: number;
387
+ height?: number;
377
388
  maxMessages?: number;
378
389
  debug?: boolean;
379
390
  expandToolOutputs?: boolean;
391
+ scrollback?: boolean;
380
392
  }
381
393
 
382
394
  export function buildMessageListView({
@@ -384,30 +396,34 @@ export function buildMessageListView({
384
396
  height,
385
397
  maxMessages = 50,
386
398
  debug = false,
387
- expandToolOutputs = false
399
+ expandToolOutputs = false,
400
+ scrollback = false
388
401
  }: MessageListProps): LayoutNode {
389
402
  const recentMessages = messages.slice(-maxMessages);
390
- const visibleMessages: Message[] = [];
391
- let remaining = height;
392
- for (let i = recentMessages.length - 1; i >= 0; i -= 1) {
393
- const msg = recentMessages[i];
394
- const estimate = estimateMessageLines(msg);
395
- if (estimate <= remaining) {
396
- visibleMessages.unshift(msg);
397
- remaining -= estimate;
398
- continue;
399
- }
400
- if (visibleMessages.length === 0) {
401
- const clipped = clipMessage(msg, remaining);
402
- if (clipped) {
403
- visibleMessages.unshift(clipped);
403
+ let visibleMessages: Message[] = recentMessages;
404
+ if (!scrollback && typeof height === 'number') {
405
+ visibleMessages = [];
406
+ let remaining = height;
407
+ for (let i = recentMessages.length - 1; i >= 0; i -= 1) {
408
+ const msg = recentMessages[i];
409
+ const estimate = estimateMessageLines(msg);
410
+ if (estimate <= remaining) {
411
+ visibleMessages.unshift(msg);
412
+ remaining -= estimate;
413
+ continue;
404
414
  }
415
+ if (visibleMessages.length === 0) {
416
+ const clipped = clipMessage(msg, remaining);
417
+ if (clipped) {
418
+ visibleMessages.unshift(clipped);
419
+ }
420
+ }
421
+ break;
405
422
  }
406
- break;
407
423
  }
408
424
 
409
425
  return box([
410
- box([], { flexGrow: 1 }),
426
+ !scrollback ? box([], { flexGrow: 1 }) : box([], {}),
411
427
  box(visibleMessages.map(msg => messageView(msg, expandToolOutputs)), { flexDirection: 'column' }),
412
428
  messages.length === 0
413
429
  ? box([text('No messages yet. Type something to begin.', { color: 'gray', dimColor: true })], {
@@ -418,9 +434,9 @@ export function buildMessageListView({
418
434
  : box([], {})
419
435
  ], {
420
436
  flexDirection: 'column',
421
- height,
437
+ height: scrollback ? undefined : height,
422
438
  paddingX: 1,
423
- flexGrow: 1,
439
+ flexGrow: scrollback ? undefined : 1,
424
440
  borderStyle: debug ? 'single' : undefined,
425
441
  borderColor: debug ? 'gray' : undefined
426
442
  });
@@ -13,6 +13,7 @@ interface StatusBarProps {
13
13
  emulationId?: string;
14
14
  inputMode?: 'queue' | 'interrupt';
15
15
  toast?: string | null;
16
+ spinnerLabel?: string | null;
16
17
  debug?: boolean;
17
18
  }
18
19
 
@@ -39,10 +40,13 @@ export function buildStatusBarView({
39
40
  emulationId,
40
41
  inputMode,
41
42
  toast,
43
+ spinnerLabel,
42
44
  debug = false
43
45
  }: StatusBarProps): LayoutNode {
44
46
  const config = getStatusConfig(state);
45
47
  const isActive = state.status !== 'idle' && state.status !== 'error';
48
+ const useSpinnerLabel = spinnerLabel && (state.status === 'thinking' || state.status === 'streaming');
49
+ const label = useSpinnerLabel ? spinnerLabel : config.label;
46
50
 
47
51
  const connectionColors = {
48
52
  connected: 'green',
@@ -64,7 +68,7 @@ export function buildStatusBarView({
64
68
  return box([
65
69
  box([
66
70
  text(isActive ? '⠋ ' : `${config.icon} `, { color: config.color }),
67
- text(config.label, { color: config.color }),
71
+ text(label, { color: config.color }),
68
72
  state.error ? text(` - ${state.error}`, { color: 'red' }) : text('', {}),
69
73
  toastLabel ? text(' • ', { color: 'gray', dimColor: true }) : text('', {}),
70
74
  toastLabel ? text(toastLabel, { color: 'yellow' }) : text('', {})
@@ -54,7 +54,16 @@ export async function runShellCommand(command: string, cwd: string): Promise<She
54
54
 
55
55
  export async function resolveWorkingDir(current: string, target?: string): Promise<string> {
56
56
  const base = current || process.cwd();
57
- const next = target && target.trim().length > 0 ? resolve(base, target) : base;
57
+ let path = target?.trim() || '';
58
+
59
+ // Expand tilde to home directory
60
+ if (path.startsWith('~/')) {
61
+ path = resolve(process.env.HOME || '', path.slice(2));
62
+ } else if (path === '~') {
63
+ path = process.env.HOME || '';
64
+ }
65
+
66
+ const next = path.length > 0 ? resolve(base, path) : base;
58
67
  const info = await stat(next);
59
68
  if (!info.isDirectory()) {
60
69
  throw new Error('Not a directory');
@@ -0,0 +1 @@
1
+ export const SPINNER_FRAMES = ['-', '\\', '|', '/'];
@@ -0,0 +1,23 @@
1
+ export const DEFAULT_SPINNER_VERBS = [
2
+ 'Thinking',
3
+ 'Reticulating splines',
4
+ 'Allocating time slices',
5
+ 'Negotiating with entropy',
6
+ 'Consulting the archives',
7
+ 'Organizing thoughts',
8
+ 'Compiling context',
9
+ 'Assembling response',
10
+ 'Tracing dependencies',
11
+ 'Resolving intent'
12
+ ];
13
+
14
+ export function parseSpinnerVerbs(input: string): string[] {
15
+ return input
16
+ .split(/[,|;\n]+/g)
17
+ .map(part => part.trim())
18
+ .filter(Boolean);
19
+ }
20
+
21
+ export function formatSpinnerVerbs(verbs: string[]): string {
22
+ return verbs.length === 0 ? '(none)' : verbs.join(', ');
23
+ }
@@ -138,7 +138,11 @@ export function formatToolStart(tool: string, args: Record<string, unknown>, emu
138
138
  return `⏺ ${label}${argsText}`;
139
139
  }
140
140
  if (style === 'codex') {
141
- return `• ${label}${argsText}`;
141
+ if (tool === 'run_command') {
142
+ const command = args.command ? String(args.command) : '';
143
+ return `! ${command}`;
144
+ }
145
+ return `⏺ ${label}${argsText}`;
142
146
  }
143
147
  return `> ${label}${argsText}`;
144
148
  }
@@ -150,7 +154,13 @@ export function formatToolEnd(tool: string, result: string, durationMs?: number,
150
154
  return `⎿ ${summary}`;
151
155
  }
152
156
  if (style === 'codex') {
153
- return ` ${summary}`;
157
+ if (tool === 'run_command') {
158
+ const { lines } = buildOutputLines(tool, result);
159
+ if (lines.length === 0) {
160
+ return ` ⎿ (No content)`;
161
+ }
162
+ }
163
+ return ` ⎿ ${summary}`;
154
164
  }
155
165
  return `< ${summary}`;
156
166
  }
@@ -0,0 +1,44 @@
1
+ const PACKAGE_NAME = 'zerg-ztc';
2
+
3
+ export interface UpdateCheck {
4
+ current: string;
5
+ latest: string;
6
+ hasUpdate: boolean;
7
+ }
8
+
9
+ function parseVersion(value: string): number[] {
10
+ return value.split('.').map(part => Number(part.replace(/[^0-9]/g, '')) || 0);
11
+ }
12
+
13
+ export function compareVersions(a: string, b: string): number {
14
+ const pa = parseVersion(a);
15
+ const pb = parseVersion(b);
16
+ const len = Math.max(pa.length, pb.length);
17
+ for (let i = 0; i < len; i += 1) {
18
+ const av = pa[i] ?? 0;
19
+ const bv = pb[i] ?? 0;
20
+ if (av > bv) return 1;
21
+ if (av < bv) return -1;
22
+ }
23
+ return 0;
24
+ }
25
+
26
+ export async function fetchLatestVersion(): Promise<string> {
27
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
28
+ headers: { 'Accept': 'application/json' }
29
+ });
30
+ if (!res.ok) {
31
+ throw new Error(`Version check failed (${res.status})`);
32
+ }
33
+ const data = await res.json() as { version?: string };
34
+ return data.version || '0.0.0';
35
+ }
36
+
37
+ export async function checkForUpdate(current: string): Promise<UpdateCheck> {
38
+ const latest = await fetchLatestVersion();
39
+ return {
40
+ current,
41
+ latest,
42
+ hasUpdate: compareVersions(latest, current) > 0
43
+ };
44
+ }
@@ -0,0 +1,15 @@
1
+ import { readFileSync } from 'fs';
2
+ import { dirname, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ export function getVersion(): string {
6
+ try {
7
+ const here = dirname(fileURLToPath(import.meta.url));
8
+ const pkgPath = resolve(here, '../../package.json');
9
+ const raw = readFileSync(pkgPath, 'utf-8');
10
+ const parsed = JSON.parse(raw) as { version?: string };
11
+ return parsed.version || '0.0.0';
12
+ } catch {
13
+ return '0.0.0';
14
+ }
15
+ }