uloop-cli 0.68.2 → 0.69.0

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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Project validation for CLI.
3
+ * Verifies that the connected Unity instance belongs to the expected project.
4
+ */
5
+
6
+ // Paths come from trusted Unity responses and validated project root, console.error for user warnings
7
+ /* eslint-disable security/detect-non-literal-fs-filename, no-console */
8
+
9
+ import assert from 'node:assert';
10
+ import { realpath } from 'fs/promises';
11
+ import { dirname } from 'path';
12
+ import { DirectUnityClient } from './direct-unity-client.js';
13
+
14
+ export class ProjectMismatchError extends Error {
15
+ constructor(
16
+ public readonly expectedProjectRoot: string,
17
+ public readonly connectedProjectRoot: string,
18
+ ) {
19
+ super('PROJECT_MISMATCH');
20
+ }
21
+ }
22
+
23
+ interface GetVersionResponse {
24
+ DataPath: string;
25
+ }
26
+
27
+ const JSON_RPC_METHOD_NOT_FOUND = -32601;
28
+
29
+ async function normalizePath(path: string): Promise<string> {
30
+ const resolved = await realpath(path);
31
+ return resolved.replace(/\/+$/, '');
32
+ }
33
+
34
+ export async function validateConnectedProject(
35
+ client: DirectUnityClient,
36
+ expectedProjectRoot: string,
37
+ ): Promise<void> {
38
+ assert(client.isConnected(), 'client must be connected before validation');
39
+
40
+ let response: GetVersionResponse;
41
+ try {
42
+ response = await client.sendRequest<GetVersionResponse>('get-version', {});
43
+ } catch (error) {
44
+ // Method not found: old uLoopMCP version without get-version tool
45
+ if (
46
+ error instanceof Error &&
47
+ (error.message.includes(`${JSON_RPC_METHOD_NOT_FOUND}`) ||
48
+ /method not found/i.test(error.message))
49
+ ) {
50
+ console.error(
51
+ 'Warning: Could not verify project identity (get-version not available). Consider updating uLoopMCP package.',
52
+ );
53
+ return;
54
+ }
55
+ throw error;
56
+ }
57
+
58
+ if (typeof response?.DataPath !== 'string' || response.DataPath.length === 0) {
59
+ console.error('Warning: Could not verify project identity (invalid get-version response).');
60
+ return;
61
+ }
62
+
63
+ const connectedProjectRoot = dirname(response.DataPath);
64
+ const normalizedExpected = await normalizePath(expectedProjectRoot);
65
+ const normalizedConnected = await normalizePath(connectedProjectRoot);
66
+
67
+ if (normalizedExpected !== normalizedConnected) {
68
+ throw new ProjectMismatchError(normalizedExpected, normalizedConnected);
69
+ }
70
+ }
@@ -11,7 +11,7 @@ export function createFrame(jsonContent: string): string {
11
11
  return `${CONTENT_LENGTH_HEADER} ${contentLength}${HEADER_SEPARATOR}${jsonContent}`;
12
12
  }
13
13
 
14
- export interface FrameParseResult {
14
+ interface FrameParseResult {
15
15
  contentLength: number;
16
16
  headerLength: number;
17
17
  isComplete: boolean;
@@ -43,7 +43,7 @@ export function parseFrameFromBuffer(data: Buffer): FrameParseResult {
43
43
  return { contentLength, headerLength, isComplete };
44
44
  }
45
45
 
46
- export interface FrameExtractionResult {
46
+ interface FrameExtractionResult {
47
47
  jsonContent: string | null;
48
48
  remainingData: Buffer;
49
49
  }
@@ -6,5 +6,5 @@
6
6
  */
7
7
  export const DEPRECATED_SKILLS: string[] = [
8
8
  'uloop-capture-window', // renamed to uloop-screenshot in v0.54.0
9
- 'uloop-get-provider-details', // internal development-only tool, not for end users
9
+ 'uloop-get-provider-details', // renamed to uloop-get-unity-search-providers
10
10
  ];
@@ -15,16 +15,16 @@ import { findUnityProjectRoot, getUnityProjectStatus } from '../project-root.js'
15
15
  import { DEPRECATED_SKILLS } from './deprecated-skills.js';
16
16
  import { loadDisabledTools } from '../tool-settings-loader.js';
17
17
 
18
- export type SkillStatus = 'installed' | 'not_installed' | 'outdated';
18
+ type SkillStatus = 'installed' | 'not_installed' | 'outdated';
19
19
 
20
- export interface SkillInfo {
20
+ interface SkillInfo {
21
21
  name: string;
22
22
  status: SkillStatus;
23
23
  path?: string;
24
24
  source?: 'bundled' | 'project';
25
25
  }
26
26
 
27
- export interface SkillDefinition {
27
+ interface SkillDefinition {
28
28
  name: string;
29
29
  toolName?: string;
30
30
  dirName: string;
@@ -132,7 +132,7 @@ function isSkillOutdated(skill: SkillDefinition, target: TargetConfig, global: b
132
132
  return false;
133
133
  }
134
134
 
135
- export function getSkillStatus(
135
+ function getSkillStatus(
136
136
  skill: SkillDefinition,
137
137
  target: TargetConfig,
138
138
  global: boolean,
@@ -290,7 +290,7 @@ function findEditorFolders(basePath: string, maxDepth: number = 2): string[] {
290
290
  return editorFolders;
291
291
  }
292
292
 
293
- export function collectProjectSkills(excludedRoots: string[] = []): SkillDefinition[] {
293
+ function collectProjectSkills(excludedRoots: string[] = []): SkillDefinition[] {
294
294
  const projectRoot = findUnityProjectRoot();
295
295
  if (!projectRoot) {
296
296
  return [];
@@ -342,7 +342,7 @@ export function getAllSkillStatuses(target: TargetConfig, global: boolean): Skil
342
342
  }));
343
343
  }
344
344
 
345
- export function installSkill(skill: SkillDefinition, target: TargetConfig, global: boolean): void {
345
+ function installSkill(skill: SkillDefinition, target: TargetConfig, global: boolean): void {
346
346
  const baseDir = global ? getGlobalSkillsDir(target) : getProjectSkillsDir(target);
347
347
  const skillDir = join(baseDir, skill.dirName);
348
348
  const skillPath = join(skillDir, target.skillFileName);
@@ -360,11 +360,7 @@ export function installSkill(skill: SkillDefinition, target: TargetConfig, globa
360
360
  }
361
361
  }
362
362
 
363
- export function uninstallSkill(
364
- skill: SkillDefinition,
365
- target: TargetConfig,
366
- global: boolean,
367
- ): boolean {
363
+ function uninstallSkill(skill: SkillDefinition, target: TargetConfig, global: boolean): boolean {
368
364
  const baseDir = global ? getGlobalSkillsDir(target) : getProjectSkillsDir(target);
369
365
  const skillDir = join(baseDir, skill.dirName);
370
366
 
@@ -376,7 +372,7 @@ export function uninstallSkill(
376
372
  return true;
377
373
  }
378
374
 
379
- export interface InstallResult {
375
+ interface InstallResult {
380
376
  installed: number;
381
377
  updated: number;
382
378
  skipped: number;
@@ -448,7 +444,7 @@ function isSkillDisabledByToolSettings(skill: SkillDefinition, disabledTools: st
448
444
  return disabledTools.includes(toolName);
449
445
  }
450
446
 
451
- export interface UninstallResult {
447
+ interface UninstallResult {
452
448
  removed: number;
453
449
  notFound: number;
454
450
  }
@@ -12,7 +12,7 @@ export interface TargetConfig {
12
12
  skillFileName: string;
13
13
  }
14
14
 
15
- export const TARGET_CONFIGS: Record<TargetId, TargetConfig> = {
15
+ const TARGET_CONFIGS: Record<TargetId, TargetConfig> = {
16
16
  claude: {
17
17
  id: 'claude',
18
18
  displayName: 'Claude Code',
package/src/spinner.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const;
9
9
  const FRAME_INTERVAL_MS = 80;
10
10
 
11
- export interface Spinner {
11
+ interface Spinner {
12
12
  update(message: string): void;
13
13
  stop(): void;
14
14
  }
package/src/tool-cache.ts CHANGED
@@ -41,7 +41,10 @@ export interface ToolsCache {
41
41
  const CACHE_DIR = '.uloop';
42
42
  const CACHE_FILE = 'tools.json';
43
43
 
44
- function getCacheDir(): string {
44
+ function getCacheDir(projectPath?: string): string {
45
+ if (projectPath !== undefined) {
46
+ return join(projectPath, CACHE_DIR);
47
+ }
45
48
  const projectRoot = findUnityProjectRoot();
46
49
  if (projectRoot === null) {
47
50
  return join(process.cwd(), CACHE_DIR);
@@ -49,8 +52,8 @@ function getCacheDir(): string {
49
52
  return join(projectRoot, CACHE_DIR);
50
53
  }
51
54
 
52
- function getCachePath(): string {
53
- return join(getCacheDir(), CACHE_FILE);
55
+ function getCachePath(projectPath?: string): string {
56
+ return join(getCacheDir(projectPath), CACHE_FILE);
54
57
  }
55
58
 
56
59
  /**
@@ -62,9 +65,10 @@ export function getDefaultTools(): ToolsCache {
62
65
 
63
66
  /**
64
67
  * Load tools from cache file, falling back to default tools if cache doesn't exist.
68
+ * When projectPath is specified, reads cache from that project directory.
65
69
  */
66
- export function loadToolsCache(): ToolsCache {
67
- const cachePath = getCachePath();
70
+ export function loadToolsCache(projectPath?: string): ToolsCache {
71
+ const cachePath = getCachePath(projectPath);
68
72
 
69
73
  if (existsSync(cachePath)) {
70
74
  try {
@@ -107,6 +111,15 @@ export function getCacheFilePath(): string {
107
111
  return getCachePath();
108
112
  }
109
113
 
114
+ /**
115
+ * Get the set of default tool names bundled with npm package.
116
+ * Used to distinguish built-in tools from third-party tools.
117
+ */
118
+ export function getDefaultToolNames(): ReadonlySet<string> {
119
+ const defaultTools: ToolsCache = getDefaultTools();
120
+ return new Set(defaultTools.tools.map((tool: ToolDefinition) => tool.name));
121
+ }
122
+
110
123
  /**
111
124
  * Get the Unity server version from cache file.
112
125
  * Returns undefined if cache doesn't exist, is corrupted, or serverVersion is missing.
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * This file exists to avoid bundling the entire package.json into the CLI bundle.
5
5
  * This version is automatically updated by release-please.
6
6
  */
7
- export const VERSION = '0.68.2'; // x-release-please-version
7
+ export const VERSION = '0.69.0'; // x-release-please-version