Skip to content

Commit 9f16c7c

Browse files
committed
update adding claude.md
1 parent 8ef0753 commit 9f16c7c

11 files changed

Lines changed: 878 additions & 47 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/sh
2+
if grep -q "caliber" .git/hooks/pre-commit 2>/dev/null; then
3+
exit 0
4+
fi
5+
FLAG="/tmp/caliber-nudge-$(echo "$PWD" | shasum | cut -c1-8)"
6+
find /tmp -maxdepth 1 -name "caliber-nudge-*" -mmin +120 -delete 2>/dev/null
7+
if [ -f "$FLAG" ]; then
8+
exit 0
9+
fi
10+
touch "$FLAG"
11+
printf '{"decision":"block","reason":"Caliber agent config sync is not set up on this machine. Tell the user: This project uses Caliber to keep agent configs in sync — it is not set up yet. Want me to run /setup-caliber? (~30 seconds)"}'

.claude/settings.json

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"hooks": {
3+
"Stop": [
4+
{
5+
"matcher": "",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": ".claude/hooks/caliber-check-sync.sh",
10+
"description": "Caliber: offer setup if not configured"
11+
}
12+
]
13+
}
14+
],
15+
"PostToolUse": [
16+
{
17+
"matcher": "",
18+
"hooks": [
19+
{
20+
"type": "command",
21+
"command": "caliber learn observe",
22+
"description": "Caliber: recording tool usage for session learning"
23+
}
24+
]
25+
}
26+
],
27+
"PostToolUseFailure": [
28+
{
29+
"matcher": "",
30+
"hooks": [
31+
{
32+
"type": "command",
33+
"command": "caliber learn observe --failure",
34+
"description": "Caliber: recording tool failure for session learning"
35+
}
36+
]
37+
}
38+
],
39+
"UserPromptSubmit": [
40+
{
41+
"matcher": "",
42+
"hooks": [
43+
{
44+
"type": "command",
45+
"command": "caliber learn observe --prompt",
46+
"description": "Caliber: recording user prompt for correction detection"
47+
}
48+
]
49+
}
50+
],
51+
"SessionEnd": [
52+
{
53+
"matcher": "",
54+
"hooks": [
55+
{
56+
"type": "command",
57+
"command": "caliber learn finalize --auto",
58+
"description": "Caliber: finalizing session learnings"
59+
}
60+
]
61+
},
62+
{
63+
"matcher": "",
64+
"hooks": [
65+
{
66+
"type": "command",
67+
"command": "caliber refresh --quiet",
68+
"description": "Caliber: auto-refreshing docs based on code changes"
69+
}
70+
]
71+
}
72+
]
73+
},
74+
"permissions": {
75+
"allow": [
76+
"Bash(git *)"
77+
]
78+
}
79+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
---
2+
name: addon-lifecycle
3+
description: Implements doEnable/doDisable lifecycle methods following the pattern in src/Plugin.php. Handles get_module_settings(), myadmin_log(), function_requirements(), vps_get_password(), run_event('parse_service_extra'), and admin email via \MyAdmin\Mail()->adminMail(). Use when user says 'add enable logic', 'implement disable', 'provision addon', or works on activation/deactivation. Do NOT use for hook registration or AddonHandler setup.
4+
---
5+
# Addon Lifecycle
6+
7+
## Critical
8+
9+
- Always call `require_once __DIR__.'/../../../../include/licenses/license.functions.inc.php'` **before** any license or activation functions in both `doEnable` and `doDisable`.
10+
- Always resolve the service ID and customer ID through `$settings['PREFIX']` — never hardcode column names.
11+
- Call `myadmin_log()` immediately after `get_module_settings()` as the first observable action in both methods.
12+
- All external functions (`activate_*`, `deactivate_*`, `directadmin_get_best_type`) must be loaded via `function_requirements()`, not `require_once`.
13+
14+
## Instructions
15+
16+
1. **Declare the method signature** — both lifecycle methods share the same signature:
17+
```php
18+
public static function doEnable(\ServiceHandler $serviceOrder, $repeatInvoiceId, $regexMatch = false)
19+
public static function doDisable(\ServiceHandler $serviceOrder, $repeatInvoiceId, $regexMatch = false)
20+
```
21+
Verify `$serviceOrder` is typed as `\ServiceHandler` before proceeding.
22+
23+
2. **Extract service info and settings** — always the first two lines of the method body:
24+
```php
25+
$serviceInfo = $serviceOrder->getServiceInfo();
26+
$settings = get_module_settings(self::$module);
27+
```
28+
All subsequent column references use `$settings['PREFIX']` as the key prefix.
29+
30+
3. **Load license functions** — immediately after step 2, before any other calls:
31+
```php
32+
require_once __DIR__.'/../../../../include/licenses/license.functions.inc.php';
33+
```
34+
35+
4. **Log the lifecycle event:**
36+
```php
37+
myadmin_log(self::$module, 'info', self::$name.' Activation', __LINE__, __FILE__, self::$module, $serviceInfo[$settings['PREFIX'].'_id']);
38+
// or '... Deactivation' for doDisable
39+
```
40+
Use `self::$name` (not a string literal) as the message prefix.
41+
42+
5. **`doEnable` only — fetch password and parse extra:**
43+
```php
44+
$pass = vps_get_password($serviceInfo[$settings['PREFIX'].'_id'], $serviceInfo[$settings['PREFIX'].'_custid']);
45+
function_requirements('directadmin_get_best_type');
46+
function_requirements('activate_directadmin'); // replace with your activation function
47+
$serviceExtra = run_event('parse_service_extra', $serviceInfo[$settings['PREFIX'].'_extra'], self::$module);
48+
$ostype = directadmin_get_best_type(self::$module, $serviceInfo[$settings['PREFIX'].'_type'], $serviceInfo, $serviceExtra);
49+
```
50+
Verify `$pass` and `$ostype` are populated before calling the activation function.
51+
52+
6. **`doEnable` only — call the provisioning function:**
53+
```php
54+
activate_directadmin(
55+
$serviceInfo[$settings['PREFIX'].'_ip'],
56+
$ostype,
57+
$pass,
58+
$GLOBALS['tf']->accounts->cross_reference($serviceInfo[$settings['PREFIX'].'_custid']),
59+
self::$module.$serviceInfo[$settings['PREFIX'].'_id']
60+
);
61+
```
62+
63+
7. **`doDisable` only — call the deprovisioning function then send admin email:**
64+
```php
65+
function_requirements('deactivate_directadmin'); // replace with your deactivation function
66+
deactivate_directadmin($serviceInfo[$settings['PREFIX'].'_ip']);
67+
$email = $settings['TBLNAME'].' ID: '.$serviceInfo[$settings['PREFIX'].'_id'].'<br>'
68+
.$settings['TBLNAME'].' Hostname: '.$serviceInfo[$settings['PREFIX'].'_hostname'].'<br>'
69+
.'Repeat Invoice: '.$repeatInvoiceId.'<br>'
70+
.'Description: '.self::$name.'<br>';
71+
$subject = $settings['TBLNAME'].' '.$serviceInfo[$settings['PREFIX'].'_id'].' Canceled '.self::$name;
72+
(new \MyAdmin\Mail())->adminMail($subject, $email, false, 'admin/vps_da_canceled.tpl');
73+
```
74+
Verify the `.tpl` path exists under `include/templates/email/admin/` before committing.
75+
76+
## Examples
77+
78+
**User says:** "Implement doEnable and doDisable for a new Imunify addon"
79+
80+
**Actions taken:**
81+
1. Add `public static $name = 'Imunify VPS Addon';` and `public static $module = 'vps';` to the Plugin class.
82+
2. Implement `doEnable` following steps 1–6, replacing `activate_directadmin``activate_imunify` and loading it via `function_requirements('activate_imunify')`.
83+
3. Implement `doDisable` following step 7, replacing `deactivate_directadmin``deactivate_imunify` and template `admin/vps_da_canceled.tpl``admin/vps_imunify_canceled.tpl`.
84+
85+
**Result:**
86+
```php
87+
public static function doEnable(\ServiceHandler $serviceOrder, $repeatInvoiceId, $regexMatch = false)
88+
{
89+
$serviceInfo = $serviceOrder->getServiceInfo();
90+
$settings = get_module_settings(self::$module);
91+
require_once __DIR__.'/../../../../include/licenses/license.functions.inc.php';
92+
myadmin_log(self::$module, 'info', self::$name.' Activation', __LINE__, __FILE__, self::$module, $serviceInfo[$settings['PREFIX'].'_id']);
93+
$pass = vps_get_password($serviceInfo[$settings['PREFIX'].'_id'], $serviceInfo[$settings['PREFIX'].'_custid']);
94+
function_requirements('activate_imunify');
95+
$serviceExtra = run_event('parse_service_extra', $serviceInfo[$settings['PREFIX'].'_extra'], self::$module);
96+
activate_imunify($serviceInfo[$settings['PREFIX'].'_ip'], $pass, $GLOBALS['tf']->accounts->cross_reference($serviceInfo[$settings['PREFIX'].'_custid']), self::$module.$serviceInfo[$settings['PREFIX'].'_id']);
97+
}
98+
```
99+
100+
## Common Issues
101+
102+
- **`Call to undefined function activate_*`**: You skipped `function_requirements('activate_*')` before calling it. Add the call immediately before the activation function call.
103+
- **`Undefined index: PREFIX_id`**: `get_module_settings()` was not called, or `self::$module` is wrong. Verify `public static $module = 'vps';` matches the registered module name.
104+
- **`require_once` path resolves to wrong location**: `__DIR__` is relative to `src/`. The path `__DIR__.'/../../../../include/licenses/license.functions.inc.php'` assumes four directory levels up to the MyAdmin root. If the plugin is not installed under `vendor/detain/myadmin-*/src/`, adjust the `../` count.
105+
- **Admin email not sent after disable**: Ensure `(new \MyAdmin\Mail())` is instantiated with `new` and the template path is relative to `include/templates/email/`. Missing template file will silently fail — confirm the `.tpl` exists.
106+
- **`vps_get_password` returns empty**: The VPS record may lack a stored password. Log `$pass` with `myadmin_log()` immediately after the call to confirm before passing to the activation function.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
name: find-skills
3+
description: Discovers and installs community skills from the public registry. Use when the user mentions a technology, framework, or task that could benefit from specialized skills not yet installed, asks 'how do I do X', 'find a skill for X', or starts work in a new technology area. Proactively suggest when the user's task involves tools or frameworks without existing skills.
4+
---
5+
6+
# Find Skills
7+
8+
Search the public skill registry for community-contributed skills
9+
relevant to the user's current task and install them into this project.
10+
11+
## Instructions
12+
13+
1. Identify the key technologies, frameworks, or task types from the
14+
user's request that might have community skills available
15+
2. Ask the user: "Would you like me to search for community skills
16+
for [identified technologies]?"
17+
3. If the user agrees, run:
18+
```bash
19+
caliber skills --query "<relevant terms>"
20+
```
21+
This outputs the top 5 matching skills with scores and descriptions.
22+
4. Present the results to the user and ask which ones to install
23+
5. Install the selected skills:
24+
```bash
25+
caliber skills --install <slug1>,<slug2>
26+
```
27+
6. Read the installed SKILL.md files to load them into your current
28+
context so you can use them immediately in this session
29+
7. Summarize what was installed and continue with the user's task
30+
31+
## Examples
32+
33+
User: "let's build a web app using React"
34+
-> "I notice you want to work with React. Would you like me to search
35+
for community skills that could help with React development?"
36+
-> If yes: run `caliber skills --query "react frontend"`
37+
-> Show the user the results, ask which to install
38+
-> Run `caliber skills --install <selected-slugs>`
39+
-> Read the installed files and continue
40+
41+
User: "help me set up Docker for this project"
42+
-> "Would you like me to search for Docker-related skills?"
43+
-> If yes: run `caliber skills --query "docker deployment"`
44+
45+
User: "I need to write tests for this Python ML pipeline"
46+
-> "Would you like me to find skills for Python ML testing?"
47+
-> If yes: run `caliber skills --query "python machine-learning testing"`
48+
49+
## When NOT to trigger
50+
51+
- The user is working within an already well-configured area
52+
- You already suggested skills for this technology in this session
53+
- The user is in the middle of urgent debugging or time-sensitive work
54+
- The technology is too generic (e.g. just "code" or "programming")
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
name: phpunit-source-test
3+
description: Writes PHPUnit 9 source-reading tests that verify PHP file structure without executing code. Use when user says 'write tests', 'add test for', or adds/modifies files in src/. Tests use file_get_contents() + assertStringContainsString/assertMatchesRegularExpression for procedural files, and ReflectionClass for class files. Do NOT use for integration tests, mock-based tests, or tests that exercise runtime behavior.
4+
---
5+
# PHPUnit Source-Reading Tests
6+
7+
## Critical
8+
9+
- Never instantiate classes or call functions from `src/` directly — they depend on heavy MyAdmin globals (`function_requirements`, DB, constants). Use `file_get_contents()` or `ReflectionClass` only.
10+
- Test namespace must be `Detain\MyAdminVpsDirectadmin\Tests` — matches `tests/` directory in `composer.json`.
11+
- Bootstrap is configured via `phpunit.xml.dist` — do not add require statements.
12+
- Run tests with: `vendor/bin/phpunit tests/ -v`
13+
14+
## Instructions
15+
16+
1. **Create the test file** in `tests/` named after the class or file under test (e.g., `tests/PluginTest.php`). Use `tests/VpsAddDirectadminTest.php` as the template for procedural files; use `tests/PluginTest.php` for class files.
17+
18+
2. **Set up the class** with the correct namespace, import, and class declaration:
19+
```php
20+
<?php
21+
namespace Detain\MyAdminVpsDirectadmin\Tests;
22+
use PHPUnit\Framework\TestCase;
23+
// For class files also add:
24+
use ReflectionClass;
25+
use ReflectionMethod;
26+
27+
class MyFileTest extends TestCase
28+
```
29+
Verify the namespace matches `tests/` autoload entry in `composer.json` before proceeding.
30+
31+
3. **For procedural `src/*.php` files** — add a `setUp()` that loads the source into `$this->source`:
32+
```php
33+
private string $filePath;
34+
private string $source;
35+
36+
protected function setUp(): void
37+
{
38+
$this->filePath = dirname(__DIR__) . '/src/my_function.php';
39+
$this->assertFileExists($this->filePath);
40+
$this->source = file_get_contents($this->filePath);
41+
}
42+
```
43+
Then write test methods using:
44+
- `$this->assertStringStartsWith('<?php', $this->source)` — PHP open tag
45+
- `$this->assertMatchesRegularExpression('/function\s+my_function\s*\(/', $this->source)` — function declaration
46+
- `$this->assertStringContainsString("function_requirements('class.Foo')", $this->source)` — required calls
47+
- `$this->assertStringNotContainsString('namespace ', $trimmed)` — no namespace in procedural files (skip comment lines with `str_starts_with($trimmed, '*')` etc.)
48+
49+
4. **For class files** — use `ReflectionClass` in `setUp()`:
50+
```php
51+
private ReflectionClass $ref;
52+
53+
protected function setUp(): void
54+
{
55+
$this->ref = new ReflectionClass(Plugin::class);
56+
}
57+
```
58+
Cover: `isInstantiable()`, `isStatic()`/`isPublic()` on methods, `getParameters()` count and names, static property values (`Plugin::$module`), `getHooks()` array keys and callable shapes `[ClassName::class, 'methodName']`.
59+
For source-level checks on class files: `file_get_contents($this->ref->getFileName())`.
60+
61+
5. **Group tests with comment separators** matching existing style:
62+
```php
63+
// ------------------------------------------------------------------
64+
// Function declaration
65+
// ------------------------------------------------------------------
66+
```
67+
Standard groups: File existence and structure · Function/class declaration · Internal calls · Docblock and metadata · File-level characteristics.
68+
69+
6. **Every test method** must be `public function testXxx(): void` with a one-line docblock describing what it asserts. Verify all assertions pass by running: `vendor/bin/phpunit tests/PluginTest.php -v`
70+
71+
## Examples
72+
73+
**User says:** "Write tests for the new `src/vps_add_cpanel.php` procedural file."
74+
75+
**Actions taken:**
76+
1. Read `src/vps_add_cpanel.php` to identify: function name, `function_requirements()` calls, method chains, constants used.
77+
2. Create `tests/VpsAddCpanelTest.php` with `setUp()` loading the file into `$this->source`.
78+
3. Add test methods: `testFileExists`, `testFileStartsWithPhpTag`, `testFileHasNoNamespace`, `testDeclaresFunctionSignature`, `testRequiresAddServiceAddon`, `testCallsLoadMethod`, `testCallsProcessMethod`, `testFileHasDocblock`.
79+
80+
**Result:**
81+
```php
82+
protected function setUp(): void
83+
{
84+
$this->filePath = dirname(__DIR__) . '/src/vps_add_cpanel.php';
85+
$this->assertFileExists($this->filePath);
86+
$this->source = file_get_contents($this->filePath);
87+
}
88+
89+
public function testDeclaresVpsAddCpanelFunction(): void
90+
{
91+
$this->assertMatchesRegularExpression(
92+
'/function\s+vps_add_cpanel\s*\(\s*\)/',
93+
$this->source
94+
);
95+
}
96+
97+
public function testRequiresAddServiceAddon(): void
98+
{
99+
$this->assertStringContainsString("function_requirements('class.AddServiceAddon')", $this->source);
100+
}
101+
```
102+
103+
## Common Issues
104+
105+
- **`Class 'Detain\MyAdminVpsDirectadmin\Plugin' not found`**: `composer install` has not been run, or autoload is stale. Run `composer dump-autoload`.
106+
- **`assertMatchesRegularExpression` not found**: You are using PHPUnit < 9. This project uses PHPUnit 9 — check `composer.json` requires `phpunit/phpunit ^9`.
107+
- **`file_get_contents(): Failed to open stream`**: Path built with `dirname(__DIR__)` is wrong. Confirm test file lives in `tests/` (one level below project root) so `dirname(__DIR__)` resolves to the repo root.
108+
- **Test passes locally but CI fails with undefined constant `VPS_DA_COST`**: You called the actual function. Source-reading tests must only use `file_get_contents()` / `assertStringContainsString` — never `include` or `require` the `src/` file under test.
109+
- **`str_starts_with` undefined**: Requires PHP 8.0+. If running PHP 7.4, replace with `strncmp($trimmed, '*', 1) === 0`.

0 commit comments

Comments
 (0)