@walwal-harness/cli 7.1.39 → 7.1.40

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,318 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Auto-setup script for Prompt Inspector
4
+
5
+ Automatically detects project type and injects the component.
6
+ Supports: Next.js (App Router / Pages Router)
7
+
8
+ Usage:
9
+ python setup.py <project_path>
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ import re
15
+ import shutil
16
+ from pathlib import Path
17
+
18
+ COMPONENT_IMPORT = "import { PromptInspector } from '@/components/PromptInspector';"
19
+
20
+ def get_version_from_file(file_path: Path) -> str | None:
21
+ """Extract version from PromptInspector.tsx file."""
22
+ if not file_path.exists():
23
+ return None
24
+ content = file_path.read_text(encoding='utf-8')
25
+ match = re.search(r"PROMPT_INSPECTOR_VERSION\s*=\s*['\"]([^'\"]+)['\"]", content)
26
+ return match.group(1) if match else '1.0.0' # Default to 1.0.0 for old versions
27
+ COMPONENT_JSX = """{process.env.NODE_ENV === 'development' && <PromptInspector />}"""
28
+
29
+ def find_project_type(project_path: Path) -> dict:
30
+ """Detect project type and find the appropriate layout file."""
31
+ result = {
32
+ 'type': None,
33
+ 'layout_file': None,
34
+ 'components_dir': None,
35
+ }
36
+
37
+ # Check for Next.js App Router
38
+ app_layout = project_path / 'app' / 'layout.tsx'
39
+ app_layout_js = project_path / 'app' / 'layout.js'
40
+
41
+ # Check for src directory structure
42
+ src_app_layout = project_path / 'src' / 'app' / 'layout.tsx'
43
+ src_app_layout_js = project_path / 'src' / 'app' / 'layout.js'
44
+
45
+ # Check for Pages Router
46
+ pages_app = project_path / 'pages' / '_app.tsx'
47
+ pages_app_js = project_path / 'pages' / '_app.js'
48
+ src_pages_app = project_path / 'src' / 'pages' / '_app.tsx'
49
+ src_pages_app_js = project_path / 'src' / 'pages' / '_app.js'
50
+
51
+ # Determine layout file
52
+ if app_layout.exists():
53
+ result['type'] = 'nextjs-app'
54
+ result['layout_file'] = app_layout
55
+ result['components_dir'] = project_path / 'components'
56
+ elif app_layout_js.exists():
57
+ result['type'] = 'nextjs-app'
58
+ result['layout_file'] = app_layout_js
59
+ result['components_dir'] = project_path / 'components'
60
+ elif src_app_layout.exists():
61
+ result['type'] = 'nextjs-app'
62
+ result['layout_file'] = src_app_layout
63
+ result['components_dir'] = project_path / 'src' / 'components'
64
+ elif src_app_layout_js.exists():
65
+ result['type'] = 'nextjs-app'
66
+ result['layout_file'] = src_app_layout_js
67
+ result['components_dir'] = project_path / 'src' / 'components'
68
+ elif pages_app.exists():
69
+ result['type'] = 'nextjs-pages'
70
+ result['layout_file'] = pages_app
71
+ result['components_dir'] = project_path / 'components'
72
+ elif pages_app_js.exists():
73
+ result['type'] = 'nextjs-pages'
74
+ result['layout_file'] = pages_app_js
75
+ result['components_dir'] = project_path / 'components'
76
+ elif src_pages_app.exists():
77
+ result['type'] = 'nextjs-pages'
78
+ result['layout_file'] = src_pages_app
79
+ result['components_dir'] = project_path / 'src' / 'components'
80
+ elif src_pages_app_js.exists():
81
+ result['type'] = 'nextjs-pages'
82
+ result['layout_file'] = src_pages_app_js
83
+ result['components_dir'] = project_path / 'src' / 'components'
84
+
85
+ return result
86
+
87
+
88
+ def compare_versions(v1: str, v2: str) -> int:
89
+ """Compare two version strings. Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2."""
90
+ def parse(v):
91
+ return [int(x) for x in v.split('.')]
92
+ p1, p2 = parse(v1), parse(v2)
93
+ for a, b in zip(p1, p2):
94
+ if a < b:
95
+ return -1
96
+ if a > b:
97
+ return 1
98
+ return 0
99
+
100
+
101
+ def copy_component(components_dir: Path, script_dir: Path, force_update: bool = False) -> bool:
102
+ """Copy PromptInspector component to project."""
103
+ components_dir.mkdir(parents=True, exist_ok=True)
104
+
105
+ source = script_dir.parent / 'assets' / 'PromptInspector.tsx'
106
+ dest = components_dir / 'PromptInspector.tsx'
107
+
108
+ if not source.exists():
109
+ print(f" Error: Source component not found at {source}")
110
+ return False
111
+
112
+ source_version = get_version_from_file(source)
113
+ dest_version = get_version_from_file(dest)
114
+
115
+ if dest.exists():
116
+ if dest_version and source_version:
117
+ comparison = compare_versions(source_version, dest_version)
118
+ if comparison > 0:
119
+ # Newer version available
120
+ print(f"\n ╔════════════════════════════════════════════════════════════╗")
121
+ print(f" ║ 🆕 UPDATE AVAILABLE ║")
122
+ print(f" ╠════════════════════════════════════════════════════════════╣")
123
+ print(f" ║ Current version: {dest_version:<10} ║")
124
+ print(f" ║ Latest version: {source_version:<10} ║")
125
+ print(f" ╚════════════════════════════════════════════════════════════╝")
126
+
127
+ if force_update:
128
+ response = 'y'
129
+ else:
130
+ response = input("\n Update to latest version? [Y/n]: ").strip().lower()
131
+
132
+ if response in ('', 'y', 'yes'):
133
+ # Backup existing
134
+ backup = dest.with_suffix('.tsx.backup')
135
+ shutil.copy(dest, backup)
136
+ print(f" 📦 Backup saved to {backup.name}")
137
+
138
+ shutil.copy(source, dest)
139
+ print(f" ✅ Updated to v{source_version}")
140
+ return True
141
+ else:
142
+ print(f" ⏭️ Skipped update (keeping v{dest_version})")
143
+ return True
144
+ elif comparison == 0:
145
+ print(f" ✅ Component is up to date (v{dest_version})")
146
+ return True
147
+ else:
148
+ print(f" ⚠️ Local version (v{dest_version}) is newer than source (v{source_version})")
149
+ return True
150
+ else:
151
+ print(f" Component already exists at {dest}")
152
+ return True
153
+
154
+ shutil.copy(source, dest)
155
+ print(f" ✅ Installed PromptInspector v{source_version}")
156
+ return True
157
+
158
+
159
+ def inject_into_layout(layout_file: Path, project_type: str) -> bool:
160
+ """Inject the component into the layout file."""
161
+ content = layout_file.read_text(encoding='utf-8')
162
+
163
+ # Check if already injected
164
+ if 'PromptInspector' in content:
165
+ print(f" Component already injected in {layout_file}")
166
+ return True
167
+
168
+ lines = content.split('\n')
169
+ new_lines = []
170
+ import_added = False
171
+ component_added = False
172
+
173
+ for i, line in enumerate(lines):
174
+ # Add import after the last import statement
175
+ if not import_added and (line.startswith('import ') or line.startswith('from ')):
176
+ new_lines.append(line)
177
+ # Check if next line is not an import
178
+ if i + 1 < len(lines) and not lines[i + 1].startswith('import ') and not lines[i + 1].startswith('from ') and lines[i + 1].strip() != '':
179
+ new_lines.append(COMPONENT_IMPORT)
180
+ import_added = True
181
+ continue
182
+
183
+ new_lines.append(line)
184
+
185
+ # For App Router: inject before closing </body> or </html>
186
+ if project_type == 'nextjs-app':
187
+ if '</body>' in line and not component_added:
188
+ indent = len(line) - len(line.lstrip())
189
+ new_lines.insert(-1, ' ' * (indent + 2) + COMPONENT_JSX)
190
+ component_added = True
191
+
192
+ # For Pages Router: inject inside Component wrapper
193
+ elif project_type == 'nextjs-pages':
194
+ if '<Component' in line and not component_added:
195
+ # Find the closing of the return statement and inject before it
196
+ pass
197
+
198
+ # If import wasn't added (no imports found), add at top
199
+ if not import_added:
200
+ new_lines.insert(0, COMPONENT_IMPORT)
201
+ new_lines.insert(1, '')
202
+
203
+ # For Pages Router, try different injection point
204
+ if project_type == 'nextjs-pages' and not component_added:
205
+ new_content = '\n'.join(new_lines)
206
+ # Find return statement and inject component
207
+ if 'return (' in new_content or 'return(' in new_content:
208
+ # Add before the closing of the main wrapper
209
+ new_content = new_content.replace(
210
+ '</Component>',
211
+ f'</Component>\n {COMPONENT_JSX}'
212
+ )
213
+ new_lines = new_content.split('\n')
214
+ component_added = True
215
+
216
+ new_content = '\n'.join(new_lines)
217
+
218
+ # Backup original
219
+ backup_file = layout_file.with_suffix(layout_file.suffix + '.backup')
220
+ if not backup_file.exists():
221
+ shutil.copy(layout_file, backup_file)
222
+ print(f" Backup created at {backup_file}")
223
+
224
+ layout_file.write_text(new_content, encoding='utf-8')
225
+ print(f" Injected component into {layout_file}")
226
+
227
+ return True
228
+
229
+
230
+ def main():
231
+ if len(sys.argv) < 2:
232
+ print("Usage: python setup.py <project_path> [options]")
233
+ print("\nAutomatically sets up Prompt Inspector in your project.")
234
+ print("\nOptions:")
235
+ print(" --check, -c Check for updates only (no installation)")
236
+ print(" --force, -y Auto-accept updates without prompting")
237
+ return 1
238
+
239
+ # Parse arguments
240
+ args = sys.argv[1:]
241
+ project_path = None
242
+ force_update = False
243
+ check_only = False
244
+
245
+ for arg in args:
246
+ if arg in ('--force', '-y'):
247
+ force_update = True
248
+ elif arg in ('--check', '-c'):
249
+ check_only = True
250
+ elif not project_path:
251
+ project_path = Path(arg).resolve()
252
+
253
+ if not project_path:
254
+ print("Error: Project path is required")
255
+ return 1
256
+
257
+ script_dir = Path(__file__).parent
258
+
259
+ if not project_path.exists():
260
+ print(f"Error: Path '{project_path}' does not exist")
261
+ return 1
262
+
263
+ print(f"Detecting project type in {project_path}...")
264
+
265
+ project_info = find_project_type(project_path)
266
+
267
+ if not project_info['type']:
268
+ print("Could not detect project type.")
269
+ print("Supported: Next.js (App Router / Pages Router)")
270
+ print("\nManual setup required. Copy PromptInspector.tsx to your components folder")
271
+ print("and import it in your root layout/app file.")
272
+ return 1
273
+
274
+ print(f"Detected: {project_info['type']}")
275
+ print(f"Layout file: {project_info['layout_file']}")
276
+ print(f"Components dir: {project_info['components_dir']}")
277
+
278
+ # Check-only mode
279
+ if check_only:
280
+ source = script_dir.parent / 'assets' / 'PromptInspector.tsx'
281
+ dest = project_info['components_dir'] / 'PromptInspector.tsx'
282
+
283
+ source_version = get_version_from_file(source)
284
+ dest_version = get_version_from_file(dest)
285
+
286
+ print(f"\n Version Check:")
287
+ print(f" ├─ Installed: {dest_version or 'Not installed'}")
288
+ print(f" └─ Latest: {source_version}")
289
+
290
+ if not dest.exists():
291
+ print("\n ⚠️ PromptInspector is not installed. Run without --check to install.")
292
+ return 1
293
+ elif dest_version and source_version and compare_versions(source_version, dest_version) > 0:
294
+ print("\n 🆕 Update available! Run without --check to update.")
295
+ return 1
296
+ else:
297
+ print("\n ✅ Up to date!")
298
+ return 0
299
+
300
+ print("\nSetting up Prompt Inspector...")
301
+
302
+ # Step 1: Copy component
303
+ if not copy_component(project_info['components_dir'], script_dir, force_update):
304
+ return 1
305
+
306
+ # Step 2: Inject into layout
307
+ if not inject_into_layout(project_info['layout_file'], project_info['type']):
308
+ return 1
309
+
310
+ print("\nSetup complete!")
311
+ print("Run your dev server and look for the toolbar in the bottom-right corner.")
312
+ print("The component only loads in development mode.")
313
+
314
+ return 0
315
+
316
+
317
+ if __name__ == '__main__':
318
+ sys.exit(main())
package/bin/init.js CHANGED
@@ -912,7 +912,7 @@ function installSkills() {
912
912
  log(` Installed: ${destName}`);
913
913
  };
914
914
 
915
- const coreHrSkills = ['ceo', 'coo', 'cdo', 'cto', 'cqo', 'ops', 'hiring', 'resource-manager', 'brick-office'];
915
+ const coreHrSkills = ['ceo', 'coo', 'cdo', 'cto', 'cqo', 'ops', 'hiring', 'resource-manager', 'brick-office', 'ops-prompt-inspector'];
916
916
  if (fs.existsSync(hrSrc)) {
917
917
  for (const skill of coreHrSkills) {
918
918
  const src = path.join(hrSrc, skill);
@@ -1552,7 +1552,7 @@ function detectMigrationNeeded() {
1552
1552
  bundleVersionStale: null, // { current, installed }
1553
1553
  };
1554
1554
 
1555
- const coreSkills = ['ceo', 'coo', 'cdo', 'cto', 'cqo', 'ops', 'hiring', 'resource-manager', 'brick-office'];
1555
+ const coreSkills = ['ceo', 'coo', 'cdo', 'cto', 'cqo', 'ops', 'hiring', 'resource-manager', 'brick-office', 'ops-prompt-inspector'];
1556
1556
  for (const skill of coreSkills) {
1557
1557
  const srcPath = path.join(PKG_ROOT, 'HR-Resource', skill, 'SKILL.md');
1558
1558
  if (!fs.existsSync(srcPath)) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "7.1.39",
3
+ "version": "7.1.40",
4
4
  "description": "Company-style AI agent harness for Claude and Codex. Installs commands, CXX agents, skills, HR-Resource hiring pool, and project-local .harness runtime state.",
5
5
  "bin": {
6
6
  "walwal-harness": "bin/init.js"