@celilo/cli 0.1.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.
- package/README.md +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Executor
|
|
3
|
+
*
|
|
4
|
+
* Executes hook scripts in a controlled environment with:
|
|
5
|
+
* - Timeout management (total + idle)
|
|
6
|
+
* - Input validation
|
|
7
|
+
* - Output validation
|
|
8
|
+
* - Structured logging
|
|
9
|
+
*
|
|
10
|
+
* Execution function (Rule 10.1) - performs side effects (script execution)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
import { isCompiledHook } from '@celilo/capabilities';
|
|
16
|
+
import {
|
|
17
|
+
type ContractHookSignature,
|
|
18
|
+
resolveContract,
|
|
19
|
+
supportedContractVersions,
|
|
20
|
+
} from '../manifest/contracts';
|
|
21
|
+
import type { HookContext, HookDefinition, HookLogger, HookResult } from './types';
|
|
22
|
+
|
|
23
|
+
/** Default total timeout: 60 seconds */
|
|
24
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
25
|
+
|
|
26
|
+
/** Default idle timeout: 30 seconds since last log message */
|
|
27
|
+
const IDLE_TIMEOUT_MS = 30_000;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate hook inputs against a contract signature.
|
|
31
|
+
*
|
|
32
|
+
* Policy function (Rule 10.1) - validates inputs.
|
|
33
|
+
*
|
|
34
|
+
* The set of required inputs comes from the contract version registered for
|
|
35
|
+
* the manifest's `celilo_contract`, NOT from the manifest itself. See
|
|
36
|
+
* `apps/celilo/src/manifest/contracts/v1.ts`.
|
|
37
|
+
*
|
|
38
|
+
* @param signature - Contract hook signature for this hook name
|
|
39
|
+
* @param inputs - Provided input values
|
|
40
|
+
* @returns Error message if validation fails, null if valid
|
|
41
|
+
*/
|
|
42
|
+
export function validateHookInputs(
|
|
43
|
+
signature: ContractHookSignature,
|
|
44
|
+
inputs: Record<string, unknown>,
|
|
45
|
+
): string | null {
|
|
46
|
+
const required = Object.entries(signature.inputs)
|
|
47
|
+
.filter(([, def]) => def.required)
|
|
48
|
+
.map(([name]) => name);
|
|
49
|
+
|
|
50
|
+
const missing = required.filter((name) => !(name in inputs));
|
|
51
|
+
if (missing.length > 0) {
|
|
52
|
+
return `Missing required hook inputs: ${missing.join(', ')}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate hook outputs against a contract signature.
|
|
60
|
+
*
|
|
61
|
+
* Policy function (Rule 10.1) - validates outputs.
|
|
62
|
+
*
|
|
63
|
+
* @param signature - Contract hook signature for this hook name
|
|
64
|
+
* @param outputs - Returned output values
|
|
65
|
+
* @returns Error message if validation fails, null if valid
|
|
66
|
+
*/
|
|
67
|
+
export function validateHookOutputs(
|
|
68
|
+
signature: ContractHookSignature,
|
|
69
|
+
outputs: Record<string, unknown>,
|
|
70
|
+
): string | null {
|
|
71
|
+
const required = Object.entries(signature.outputs)
|
|
72
|
+
.filter(([, def]) => def.required)
|
|
73
|
+
.map(([name]) => name);
|
|
74
|
+
|
|
75
|
+
const missing = required.filter((name) => !(name in outputs));
|
|
76
|
+
if (missing.length > 0) {
|
|
77
|
+
return `Hook did not return required outputs: ${missing.join(', ')}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve the absolute path to a hook script
|
|
85
|
+
*
|
|
86
|
+
* Policy function (Rule 10.1) - validates path
|
|
87
|
+
*
|
|
88
|
+
* @param modulePath - Absolute path to module directory
|
|
89
|
+
* @param scriptPath - Relative script path from manifest
|
|
90
|
+
* @returns Resolved absolute path
|
|
91
|
+
*/
|
|
92
|
+
export function resolveHookScript(modulePath: string, scriptPath: string): string {
|
|
93
|
+
const resolved = resolve(modulePath, scriptPath);
|
|
94
|
+
|
|
95
|
+
// Security: ensure resolved path is within module directory
|
|
96
|
+
const normalizedModule = resolve(modulePath);
|
|
97
|
+
if (!resolved.startsWith(normalizedModule)) {
|
|
98
|
+
throw new Error(`Hook script path escapes module directory: ${scriptPath}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return resolved;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Execute a hook script
|
|
106
|
+
*
|
|
107
|
+
* Execution function (Rule 10.1) - performs side effects
|
|
108
|
+
*
|
|
109
|
+
* @param scriptPath - Absolute path to the hook script
|
|
110
|
+
* @param context - Hook context with config, secrets, logger, and inputs
|
|
111
|
+
* @param timeoutMs - Total timeout in milliseconds
|
|
112
|
+
* @returns Hook result with outputs
|
|
113
|
+
*/
|
|
114
|
+
export async function executeHookScript(
|
|
115
|
+
scriptPath: string,
|
|
116
|
+
context: HookContext,
|
|
117
|
+
timeoutMs: number = DEFAULT_TIMEOUT_MS,
|
|
118
|
+
): Promise<Record<string, unknown>> {
|
|
119
|
+
if (!existsSync(scriptPath)) {
|
|
120
|
+
throw new Error(`Hook script not found: ${scriptPath}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Create an idle timeout tracker
|
|
124
|
+
let lastActivity = Date.now();
|
|
125
|
+
const originalLogger = context.logger;
|
|
126
|
+
|
|
127
|
+
// Wrap logger to track activity for idle timeout
|
|
128
|
+
const trackedLogger: HookLogger = {
|
|
129
|
+
info(message: string) {
|
|
130
|
+
lastActivity = Date.now();
|
|
131
|
+
originalLogger.info(message);
|
|
132
|
+
},
|
|
133
|
+
warn(message: string) {
|
|
134
|
+
lastActivity = Date.now();
|
|
135
|
+
originalLogger.warn(message);
|
|
136
|
+
},
|
|
137
|
+
error(message: string) {
|
|
138
|
+
lastActivity = Date.now();
|
|
139
|
+
originalLogger.error(message);
|
|
140
|
+
},
|
|
141
|
+
success(message: string) {
|
|
142
|
+
lastActivity = Date.now();
|
|
143
|
+
originalLogger.success(message);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const trackedContext: HookContext = {
|
|
148
|
+
...context,
|
|
149
|
+
logger: trackedLogger,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Execute with timeout — disable idle timeout in debug mode
|
|
153
|
+
const promises: Promise<Record<string, unknown>>[] = [
|
|
154
|
+
runScript(scriptPath, trackedContext),
|
|
155
|
+
createTimeoutPromise(timeoutMs, 'total'),
|
|
156
|
+
];
|
|
157
|
+
if (!context.debug) {
|
|
158
|
+
promises.push(createIdleTimeoutPromise(() => Date.now() - lastActivity, IDLE_TIMEOUT_MS));
|
|
159
|
+
}
|
|
160
|
+
const result = await Promise.race(promises);
|
|
161
|
+
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Run a hook script by dynamically importing it.
|
|
167
|
+
*
|
|
168
|
+
* HOOK_API_V2 Phase 8 (D8): the executor only accepts hook scripts that
|
|
169
|
+
* use `defineHook` from `@celilo/capabilities`. The compiled output
|
|
170
|
+
* carries a brand symbol (`CELILO_HOOK_BRAND`) that this function
|
|
171
|
+
* checks via `isCompiledHook`. Raw `async function(context)` exports are
|
|
172
|
+
* rejected with a clear error pointing at the migration guide — no more
|
|
173
|
+
* silent failures from forgotten brand registration.
|
|
174
|
+
*
|
|
175
|
+
* @param scriptPath - Absolute path to script
|
|
176
|
+
* @param context - Hook context
|
|
177
|
+
* @returns Script outputs
|
|
178
|
+
*/
|
|
179
|
+
async function runScript(
|
|
180
|
+
scriptPath: string,
|
|
181
|
+
context: HookContext,
|
|
182
|
+
): Promise<Record<string, unknown>> {
|
|
183
|
+
const module = await import(scriptPath);
|
|
184
|
+
|
|
185
|
+
if (typeof module.default !== 'function') {
|
|
186
|
+
throw new Error(`Hook script must export a default function: ${scriptPath}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!isCompiledHook(module.default)) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Hook script ${scriptPath} does not use defineHook(). As of HOOK_API_V2 Phase 8, all hook scripts must wrap their handler with defineHook from @celilo/capabilities so the executor can verify the brand and apply pre-flight checks. See design/MODULE_DEVELOPMENT_GUIDE.md "Hooks" section for the migration pattern.`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const result = await module.default(context);
|
|
196
|
+
|
|
197
|
+
if (result === null || result === undefined) {
|
|
198
|
+
return {};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (typeof result !== 'object' || Array.isArray(result)) {
|
|
202
|
+
throw new Error('Hook script must return an object (or nothing)');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result as Record<string, unknown>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a promise that rejects after a total timeout
|
|
210
|
+
*/
|
|
211
|
+
function createTimeoutPromise(timeoutMs: number, type: string): Promise<never> {
|
|
212
|
+
return new Promise((_, reject) => {
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
reject(new Error(`Hook ${type} timeout exceeded (${Math.round(timeoutMs / 1000)}s)`));
|
|
215
|
+
}, timeoutMs);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create a promise that rejects when idle timeout is exceeded
|
|
221
|
+
* Polls every 5 seconds to check idle duration
|
|
222
|
+
*/
|
|
223
|
+
function createIdleTimeoutPromise(
|
|
224
|
+
getIdleDuration: () => number,
|
|
225
|
+
idleTimeoutMs: number,
|
|
226
|
+
): Promise<never> {
|
|
227
|
+
return new Promise((_, reject) => {
|
|
228
|
+
const interval = setInterval(() => {
|
|
229
|
+
if (getIdleDuration() >= idleTimeoutMs) {
|
|
230
|
+
clearInterval(interval);
|
|
231
|
+
reject(
|
|
232
|
+
new Error(
|
|
233
|
+
`Hook idle timeout exceeded (no log output for ${Math.round(idleTimeoutMs / 1000)}s)`,
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}, 5000);
|
|
238
|
+
|
|
239
|
+
// Don't block process exit
|
|
240
|
+
if (interval.unref) {
|
|
241
|
+
interval.unref();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface InvokeHookOptions {
|
|
247
|
+
debug?: boolean;
|
|
248
|
+
/** Pre-loaded capability function interfaces to inject into context */
|
|
249
|
+
capabilities?: Record<string, unknown>;
|
|
250
|
+
/**
|
|
251
|
+
* Names of capabilities the hook's module declares in
|
|
252
|
+
* `requires.capabilities`. The executor checks each one is present in
|
|
253
|
+
* `capabilities` before invoking the hook handler and fails fast with a
|
|
254
|
+
* clear error if any are missing. Added in HOOK_API_V2 Phase 3 (D3).
|
|
255
|
+
*
|
|
256
|
+
* Optional for backwards compatibility — callers that don't pass this
|
|
257
|
+
* skip the pre-flight check, matching the pre-Phase-3 behavior.
|
|
258
|
+
*/
|
|
259
|
+
requiredCapabilities?: string[];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Verify every required capability is present in the loaded capability map.
|
|
264
|
+
*
|
|
265
|
+
* Policy function (Rule 10.1) — pure check, no side effects.
|
|
266
|
+
*
|
|
267
|
+
* @returns Error message listing missing capabilities, or null if all present
|
|
268
|
+
*/
|
|
269
|
+
export function checkRequiredCapabilities(
|
|
270
|
+
hookName: string,
|
|
271
|
+
requiredCapabilities: string[],
|
|
272
|
+
loadedCapabilities: Record<string, unknown>,
|
|
273
|
+
): string | null {
|
|
274
|
+
const missing = requiredCapabilities.filter((name) => !(name in loadedCapabilities));
|
|
275
|
+
if (missing.length === 0) return null;
|
|
276
|
+
|
|
277
|
+
const lines = [
|
|
278
|
+
`Hook '${hookName}' requires capability ${missing.length === 1 ? '' : 'capabilities '}${missing.map((n) => `'${n}'`).join(', ')} but no provider${missing.length === 1 ? ' is' : 's are'} loaded.`,
|
|
279
|
+
`Install a module that provides ${missing.length === 1 ? missing[0] : 'each missing capability'} and retry.`,
|
|
280
|
+
];
|
|
281
|
+
return lines.join(' ');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Find the most recently created screenshot in a directory
|
|
286
|
+
*
|
|
287
|
+
* @param dir - Directory to scan
|
|
288
|
+
* @param since - Only consider files created after this timestamp (ms)
|
|
289
|
+
* @returns Path to screenshot, or undefined
|
|
290
|
+
*/
|
|
291
|
+
function findRecentScreenshot(dir: string, since: number): string | undefined {
|
|
292
|
+
if (!existsSync(dir)) return undefined;
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.png'));
|
|
296
|
+
for (const file of files) {
|
|
297
|
+
const fullPath = join(dir, file);
|
|
298
|
+
const stat = statSync(fullPath);
|
|
299
|
+
if (stat.mtimeMs >= since) {
|
|
300
|
+
return fullPath;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
// Best effort — don't mask the original error
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Execute a named hook for a module
|
|
311
|
+
*
|
|
312
|
+
* Orchestration function - coordinates validation, resolution, and execution.
|
|
313
|
+
*
|
|
314
|
+
* Resolves the hook's inputs/outputs signature from the contract registry
|
|
315
|
+
* keyed by `contractVersion`, validates the provided inputs against it,
|
|
316
|
+
* runs the script, and validates the returned outputs.
|
|
317
|
+
*
|
|
318
|
+
* @param modulePath - Absolute path to module directory
|
|
319
|
+
* @param hookName - Name of the hook to execute
|
|
320
|
+
* @param contractVersion - Celilo contract version from `manifest.celilo_contract`
|
|
321
|
+
* @param definition - Hook definition from manifest (script + timeout only)
|
|
322
|
+
* @param inputs - Input values for the hook
|
|
323
|
+
* @param config - Module configuration values
|
|
324
|
+
* @param secrets - Module secret values (decrypted)
|
|
325
|
+
* @param logger - Logger instance
|
|
326
|
+
* @param options - Debug and other options
|
|
327
|
+
* @returns Hook result
|
|
328
|
+
*/
|
|
329
|
+
export async function invokeHook(
|
|
330
|
+
modulePath: string,
|
|
331
|
+
hookName: string,
|
|
332
|
+
contractVersion: string,
|
|
333
|
+
definition: HookDefinition,
|
|
334
|
+
inputs: Record<string, unknown>,
|
|
335
|
+
config: Record<string, unknown>,
|
|
336
|
+
secrets: Record<string, string>,
|
|
337
|
+
logger: HookLogger,
|
|
338
|
+
options: InvokeHookOptions = {},
|
|
339
|
+
): Promise<HookResult> {
|
|
340
|
+
const startTime = Date.now();
|
|
341
|
+
const debug = options.debug ?? false;
|
|
342
|
+
|
|
343
|
+
// Resolve the hook's contract signature
|
|
344
|
+
const contract = resolveContract(contractVersion);
|
|
345
|
+
if (!contract) {
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
outputs: {},
|
|
349
|
+
error: `Unsupported celilo_contract version '${contractVersion}'. Supported: ${supportedContractVersions().join(', ')}`,
|
|
350
|
+
duration: Date.now() - startTime,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const signature = contract.hooks[hookName];
|
|
355
|
+
if (!signature) {
|
|
356
|
+
return {
|
|
357
|
+
success: false,
|
|
358
|
+
outputs: {},
|
|
359
|
+
error: `Hook '${hookName}' is not part of celilo_contract ${contractVersion}`,
|
|
360
|
+
duration: Date.now() - startTime,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Validate inputs against the contract signature
|
|
365
|
+
const inputError = validateHookInputs(signature, inputs);
|
|
366
|
+
if (inputError) {
|
|
367
|
+
return {
|
|
368
|
+
success: false,
|
|
369
|
+
outputs: {},
|
|
370
|
+
error: inputError,
|
|
371
|
+
duration: Date.now() - startTime,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Resolve script path
|
|
376
|
+
let scriptPath: string;
|
|
377
|
+
try {
|
|
378
|
+
scriptPath = resolveHookScript(modulePath, definition.script);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return {
|
|
381
|
+
success: false,
|
|
382
|
+
outputs: {},
|
|
383
|
+
error: error instanceof Error ? error.message : String(error),
|
|
384
|
+
duration: Date.now() - startTime,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Prepare screenshot directory
|
|
389
|
+
const screenshotDir = join(modulePath, 'screenshots');
|
|
390
|
+
mkdirSync(screenshotDir, { recursive: true });
|
|
391
|
+
|
|
392
|
+
// Build context
|
|
393
|
+
const loadedCapabilities = options.capabilities ?? {};
|
|
394
|
+
const context: HookContext = {
|
|
395
|
+
...inputs,
|
|
396
|
+
config,
|
|
397
|
+
secrets,
|
|
398
|
+
logger,
|
|
399
|
+
debug,
|
|
400
|
+
screenshotDir,
|
|
401
|
+
capabilities: loadedCapabilities,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Pre-flight: every required capability must be loaded (HOOK_API_V2 D3).
|
|
405
|
+
// Fail before invoking the handler so the script never sees a missing
|
|
406
|
+
// capability. Skipped if the caller didn't pass requiredCapabilities,
|
|
407
|
+
// for backwards compatibility with pre-Phase-3 invocation sites.
|
|
408
|
+
if (options.requiredCapabilities && options.requiredCapabilities.length > 0) {
|
|
409
|
+
const capError = checkRequiredCapabilities(
|
|
410
|
+
hookName,
|
|
411
|
+
options.requiredCapabilities,
|
|
412
|
+
loadedCapabilities,
|
|
413
|
+
);
|
|
414
|
+
if (capError) {
|
|
415
|
+
return {
|
|
416
|
+
success: false,
|
|
417
|
+
outputs: {},
|
|
418
|
+
error: capError,
|
|
419
|
+
duration: Date.now() - startTime,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// In debug mode, use much longer timeouts for interactive debugging
|
|
425
|
+
const timeoutMs = debug
|
|
426
|
+
? 600_000 // 10 minutes
|
|
427
|
+
: (definition.timeout ?? DEFAULT_TIMEOUT_MS);
|
|
428
|
+
|
|
429
|
+
// Execute
|
|
430
|
+
try {
|
|
431
|
+
logger.info(`Executing hook: ${hookName}`);
|
|
432
|
+
const outputs = await executeHookScript(scriptPath, context, timeoutMs);
|
|
433
|
+
|
|
434
|
+
// Validate outputs against the contract signature
|
|
435
|
+
const outputError = validateHookOutputs(signature, outputs);
|
|
436
|
+
if (outputError) {
|
|
437
|
+
return {
|
|
438
|
+
success: false,
|
|
439
|
+
outputs,
|
|
440
|
+
error: outputError,
|
|
441
|
+
duration: Date.now() - startTime,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
logger.success(`Hook ${hookName} completed successfully`);
|
|
446
|
+
return {
|
|
447
|
+
success: true,
|
|
448
|
+
outputs,
|
|
449
|
+
duration: Date.now() - startTime,
|
|
450
|
+
};
|
|
451
|
+
} catch (error) {
|
|
452
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
453
|
+
logger.error(`Hook ${hookName} failed: ${message}`);
|
|
454
|
+
|
|
455
|
+
// Check for screenshots captured by the hook
|
|
456
|
+
const screenshotPath = findRecentScreenshot(screenshotDir, startTime);
|
|
457
|
+
if (screenshotPath) {
|
|
458
|
+
logger.info(`Screenshot saved: ${screenshotPath}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
success: false,
|
|
463
|
+
outputs: {},
|
|
464
|
+
error: message,
|
|
465
|
+
screenshotPath,
|
|
466
|
+
duration: Date.now() - startTime,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { createCapturingLogger, createConsoleLogger } from './logger';
|
|
3
|
+
|
|
4
|
+
describe('Hook Logger', () => {
|
|
5
|
+
describe('createCapturingLogger', () => {
|
|
6
|
+
test('captures info messages', () => {
|
|
7
|
+
const { logger, messages } = createCapturingLogger();
|
|
8
|
+
logger.info('test message');
|
|
9
|
+
expect(messages).toEqual([{ level: 'info', message: 'test message' }]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('captures warn messages', () => {
|
|
13
|
+
const { logger, messages } = createCapturingLogger();
|
|
14
|
+
logger.warn('warning message');
|
|
15
|
+
expect(messages).toEqual([{ level: 'warn', message: 'warning message' }]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('captures error messages', () => {
|
|
19
|
+
const { logger, messages } = createCapturingLogger();
|
|
20
|
+
logger.error('error message');
|
|
21
|
+
expect(messages).toEqual([{ level: 'error', message: 'error message' }]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('captures success messages', () => {
|
|
25
|
+
const { logger, messages } = createCapturingLogger();
|
|
26
|
+
logger.success('success message');
|
|
27
|
+
expect(messages).toEqual([{ level: 'success', message: 'success message' }]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('captures messages in order', () => {
|
|
31
|
+
const { logger, messages } = createCapturingLogger();
|
|
32
|
+
logger.info('first');
|
|
33
|
+
logger.warn('second');
|
|
34
|
+
logger.error('third');
|
|
35
|
+
logger.success('fourth');
|
|
36
|
+
|
|
37
|
+
expect(messages).toHaveLength(4);
|
|
38
|
+
expect(messages[0]).toEqual({ level: 'info', message: 'first' });
|
|
39
|
+
expect(messages[1]).toEqual({ level: 'warn', message: 'second' });
|
|
40
|
+
expect(messages[2]).toEqual({ level: 'error', message: 'third' });
|
|
41
|
+
expect(messages[3]).toEqual({ level: 'success', message: 'fourth' });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('createConsoleLogger', () => {
|
|
46
|
+
test('creates a logger with all methods', () => {
|
|
47
|
+
const logger = createConsoleLogger('test-module', 'container_created');
|
|
48
|
+
expect(typeof logger.info).toBe('function');
|
|
49
|
+
expect(typeof logger.warn).toBe('function');
|
|
50
|
+
expect(typeof logger.error).toBe('function');
|
|
51
|
+
expect(typeof logger.success).toBe('function');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Logger
|
|
3
|
+
*
|
|
4
|
+
* Provides a structured logger for hook scripts to report progress
|
|
5
|
+
* back to the CLI. Integrates with FuelGauge for visual feedback.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FuelGauge } from '../cli/fuel-gauge';
|
|
9
|
+
import type { HookLogger } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a hook logger that outputs to a FuelGauge progress indicator
|
|
13
|
+
*
|
|
14
|
+
* @param gauge - FuelGauge instance for visual output
|
|
15
|
+
* @param moduleId - Module ID for log prefix
|
|
16
|
+
* @param hookName - Hook name for log prefix
|
|
17
|
+
* @returns HookLogger instance
|
|
18
|
+
*/
|
|
19
|
+
export function createGaugeLogger(
|
|
20
|
+
gauge: FuelGauge,
|
|
21
|
+
moduleId: string,
|
|
22
|
+
hookName: string,
|
|
23
|
+
): HookLogger {
|
|
24
|
+
const prefix = `[${moduleId}:${hookName}]`;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
info(message: string) {
|
|
28
|
+
gauge.addOutput(`${prefix} ${message}`);
|
|
29
|
+
},
|
|
30
|
+
warn(message: string) {
|
|
31
|
+
gauge.addOutput(`${prefix} ⚠ ${message}`);
|
|
32
|
+
},
|
|
33
|
+
error(message: string) {
|
|
34
|
+
gauge.addOutput(`${prefix} ✗ ${message}`);
|
|
35
|
+
},
|
|
36
|
+
success(message: string) {
|
|
37
|
+
gauge.addOutput(`${prefix} ✓ ${message}`);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a hook logger that outputs to console (for testing/debugging)
|
|
44
|
+
*
|
|
45
|
+
* @param moduleId - Module ID for log prefix
|
|
46
|
+
* @param hookName - Hook name for log prefix
|
|
47
|
+
* @returns HookLogger instance
|
|
48
|
+
*/
|
|
49
|
+
export function createConsoleLogger(moduleId: string, hookName: string): HookLogger {
|
|
50
|
+
const prefix = `[${moduleId}:${hookName}]`;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
info(message: string) {
|
|
54
|
+
console.log(`${prefix} ${message}`);
|
|
55
|
+
},
|
|
56
|
+
warn(message: string) {
|
|
57
|
+
console.warn(`${prefix} ⚠ ${message}`);
|
|
58
|
+
},
|
|
59
|
+
error(message: string) {
|
|
60
|
+
console.error(`${prefix} ✗ ${message}`);
|
|
61
|
+
},
|
|
62
|
+
success(message: string) {
|
|
63
|
+
console.log(`${prefix} ✓ ${message}`);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a hook logger that captures messages for testing
|
|
70
|
+
*
|
|
71
|
+
* @returns Object with logger and captured messages array
|
|
72
|
+
*/
|
|
73
|
+
export function createCapturingLogger(): {
|
|
74
|
+
logger: HookLogger;
|
|
75
|
+
messages: Array<{ level: string; message: string }>;
|
|
76
|
+
} {
|
|
77
|
+
const messages: Array<{ level: string; message: string }> = [];
|
|
78
|
+
|
|
79
|
+
const logger: HookLogger = {
|
|
80
|
+
info(message: string) {
|
|
81
|
+
messages.push({ level: 'info', message });
|
|
82
|
+
},
|
|
83
|
+
warn(message: string) {
|
|
84
|
+
messages.push({ level: 'warn', message });
|
|
85
|
+
},
|
|
86
|
+
error(message: string) {
|
|
87
|
+
messages.push({ level: 'error', message });
|
|
88
|
+
},
|
|
89
|
+
success(message: string) {
|
|
90
|
+
messages.push({ level: 'success', message });
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return { logger, messages };
|
|
95
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test fixture: hook script that throws an error. Migrated to
|
|
3
|
+
* defineHook in HOOK_API_V2 Phase 8.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { defineHook } from '@celilo/capabilities';
|
|
7
|
+
|
|
8
|
+
export default defineHook({
|
|
9
|
+
requires: [],
|
|
10
|
+
handler: async () => {
|
|
11
|
+
throw new Error('Hook execution failed: simulated error');
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test fixture: successful hook script. Migrated to defineHook in
|
|
3
|
+
* HOOK_API_V2 Phase 8 — the executor now requires the brand.
|
|
4
|
+
*
|
|
5
|
+
* Uses `hook: 'container_created'` because that hook's contract output
|
|
6
|
+
* is `SecretCapturingHookOutput` (Record<string, unknown> | void), so
|
|
7
|
+
* returning an `api_key` field is allowed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { defineHook } from '@celilo/capabilities';
|
|
11
|
+
|
|
12
|
+
export default defineHook({
|
|
13
|
+
hook: 'container_created',
|
|
14
|
+
requires: [],
|
|
15
|
+
handler: async (ctx) => {
|
|
16
|
+
ctx.logger.info('Starting test hook');
|
|
17
|
+
ctx.logger.success('Test hook completed');
|
|
18
|
+
return { api_key: `test-key-for-${(ctx.vps_ip as string | undefined) ?? 'unknown'}` };
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test fixture: a default-exported function that does NOT use defineHook.
|
|
3
|
+
*
|
|
4
|
+
* The executor's brand check (HOOK_API_V2 Phase 8 / D8) must reject
|
|
5
|
+
* this with a clear error pointing at the migration guide. Used by
|
|
6
|
+
* executor.test.ts to verify the brand-enforcement code path.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export default async function unbrandedHook() {
|
|
10
|
+
return { ok: true };
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test fixture: hook script that returns nothing. Migrated to
|
|
3
|
+
* defineHook in HOOK_API_V2 Phase 8.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { defineHook } from '@celilo/capabilities';
|
|
7
|
+
|
|
8
|
+
export default defineHook({
|
|
9
|
+
requires: [],
|
|
10
|
+
handler: async (ctx) => {
|
|
11
|
+
ctx.logger.info('Void hook ran');
|
|
12
|
+
},
|
|
13
|
+
});
|