@powerhousedao/ph-clint-cli 0.1.0-dev.8 → 0.1.0-dev.81
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/dist/cli.js +52 -10
- package/dist/codegen/actions.d.ts +46 -0
- package/dist/codegen/actions.js +424 -0
- package/dist/codegen/builders/agent-ts.d.ts +4 -4
- package/dist/codegen/builders/agent-ts.js +92 -2
- package/dist/codegen/builders/app-index-ts.d.ts +3 -10
- package/dist/codegen/builders/app-index-ts.js +30 -20
- package/dist/codegen/builders/cli-package-json.js +46 -14
- package/dist/codegen/builders/cli-ts.js +87 -49
- package/dist/codegen/builders/config-ts.d.ts +1 -1
- package/dist/codegen/builders/config-ts.js +2 -8
- package/dist/codegen/builders/demo-agent-ts.d.ts +8 -0
- package/dist/codegen/builders/demo-agent-ts.js +50 -0
- package/dist/codegen/builders/eslint-config-js.d.ts +1 -1
- package/dist/codegen/builders/eslint-config-js.js +7 -4
- package/dist/codegen/builders/framework-gen-ts.d.ts +11 -6
- package/dist/codegen/builders/framework-gen-ts.js +73 -21
- package/dist/codegen/builders/framework-ts.js +33 -7
- package/dist/codegen/builders/index.d.ts +10 -2
- package/dist/codegen/builders/index.js +30 -6
- package/dist/codegen/builders/manifest-json.d.ts +6 -0
- package/dist/codegen/builders/manifest-json.js +31 -0
- package/dist/codegen/builders/mastra-index-ts.d.ts +3 -0
- package/dist/codegen/builders/mastra-index-ts.js +47 -5
- package/dist/codegen/builders/publish-config-js.d.ts +8 -0
- package/dist/codegen/builders/publish-config-js.js +33 -0
- package/dist/codegen/builders/readme-md.js +7 -6
- package/dist/codegen/builders/root-package-json.js +11 -12
- package/dist/codegen/builders/smoke-test-ts.d.ts +2 -0
- package/dist/codegen/builders/smoke-test-ts.js +15 -0
- package/dist/codegen/exec.d.ts +0 -18
- package/dist/codegen/exec.js +0 -27
- package/dist/codegen/generated.d.ts +17 -0
- package/dist/codegen/generated.js +49 -0
- package/dist/codegen/index.d.ts +8 -36
- package/dist/codegen/index.js +6 -293
- package/dist/codegen/scaffold.d.ts +41 -3
- package/dist/codegen/scaffold.js +106 -16
- package/dist/commands/clint-project-build.d.ts +77 -1
- package/dist/commands/clint-project-build.js +11 -28
- package/dist/commands/clint-project-init.d.ts +128 -10
- package/dist/commands/clint-project-init.js +72 -47
- package/dist/commands/clint-project-publish.d.ts +78 -1
- package/dist/commands/clint-project-publish.js +6 -4
- package/dist/commands/clint-project-regen.d.ts +77 -1
- package/dist/commands/clint-project-regen.js +26 -2
- package/dist/commands/clint-skills-sync.d.ts +86 -0
- package/dist/commands/clint-skills-sync.js +56 -0
- package/dist/framework.d.ts +322 -60
- package/dist/framework.gen.d.ts +50 -10
- package/dist/framework.js +1 -5
- package/{prompts/agent-profiles/AgentBase.md → dist/gen/agent-profiles/PhClintDevAgent.md} +1 -1
- package/dist/gen/skills/cli-setup/.cli-docs.md +135 -0
- package/dist/gen/skills/cli-setup/SKILL.md +125 -0
- package/dist/gen/skills/cli-setup/references/00.assess-cli-needs.md +53 -0
- package/dist/gen/skills/cli-setup/references/01.define-config-schema.md +78 -0
- package/dist/gen/skills/cli-setup/references/02.wire-cli.md +141 -0
- package/dist/gen/skills/cli-setup/references/03.add-entrypoint.md +128 -0
- package/dist/gen/skills/cli-setup/references/expected-outcome.md +3 -0
- package/dist/gen/skills/command-definition/.cli-docs.md +84 -0
- package/dist/gen/skills/command-definition/SKILL.md +99 -0
- package/dist/gen/skills/command-definition/references/00.assess-requirements.md +58 -0
- package/dist/gen/skills/command-definition/references/01.define-schema.md +72 -0
- package/dist/gen/skills/command-definition/references/02.implement-execute.md +118 -0
- package/dist/gen/skills/command-definition/references/03.add-prompting.md +54 -0
- package/dist/gen/skills/command-definition/references/expected-outcome.md +3 -0
- package/dist/gen/skills/service-definition/.cli-docs.md +214 -0
- package/dist/gen/skills/service-definition/SKILL.md +148 -0
- package/dist/gen/skills/service-definition/references/00.assess-service.md +69 -0
- package/dist/gen/skills/service-definition/references/01.define-service.md +166 -0
- package/dist/gen/skills/service-definition/references/02.add-preflight.md +132 -0
- package/dist/gen/skills/service-definition/references/03.add-project-scanner.md +82 -0
- package/dist/gen/skills/service-definition/references/04.wire-events.md +120 -0
- package/dist/gen/skills/service-definition/references/expected-outcome.md +3 -0
- package/dist/main.js +0 -0
- package/dist/services/clint-project.js +21 -0
- package/dist/skills/sync.d.ts +40 -0
- package/dist/skills/sync.js +119 -0
- package/dist/spec/ensure-document.d.ts +17 -0
- package/dist/spec/ensure-document.js +56 -0
- package/dist/spec/file.d.ts +1 -4
- package/dist/spec/file.js +1 -34
- package/dist/spec/from-document.d.ts +4 -1
- package/dist/spec/from-document.js +4 -16
- package/dist/spec/types.d.ts +1 -62
- package/dist/spec/types.js +1 -112
- package/dist/triggers/publish-trigger.d.ts +81 -0
- package/dist/triggers/publish-trigger.js +137 -0
- package/dist/triggers/spec-change.d.ts +60 -18
- package/dist/triggers/spec-change.js +154 -101
- package/package.json +13 -18
- package/dist/codegen/builders/publish-config-ts.d.ts +0 -6
- package/dist/codegen/builders/publish-config-ts.js +0 -18
- package/dist/commands/init.d.ts +0 -57
- package/dist/commands/init.js +0 -168
- package/dist/commands/regen.d.ts +0 -18
- package/dist/commands/regen.js +0 -68
- package/prompts/skills-ext/.gitkeep +0 -0
- package/prompts/skills-tpl/.gitkeep +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Phase 2: Add Preflight Validation Checks
|
|
2
|
+
|
|
3
|
+
Add checks that run before the service process spawns. These catch common failure modes early with clear error messages.
|
|
4
|
+
|
|
5
|
+
## Steps
|
|
6
|
+
|
|
7
|
+
### 1. Add workdir check
|
|
8
|
+
|
|
9
|
+
Use `checkWorkdir()` when the service requires specific directory structure:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { checkWorkdir } from '../framework.js';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
preflight: [
|
|
17
|
+
checkWorkdir(
|
|
18
|
+
(cwd) => fs.existsSync(path.join(cwd, 'package.json')),
|
|
19
|
+
'Not a Node.js project',
|
|
20
|
+
'Run {id}-start --workdir <project-dir>, or create a project first',
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The `test` function receives the resolved working directory. Return `true` if valid.
|
|
26
|
+
|
|
27
|
+
You can check for multiple files:
|
|
28
|
+
```typescript
|
|
29
|
+
checkWorkdir(
|
|
30
|
+
(cwd) => fs.existsSync(path.join(cwd, 'powerhouse.config.ts'))
|
|
31
|
+
|| fs.existsSync(path.join(cwd, 'powerhouse.config.json')),
|
|
32
|
+
'Not a Reactor Package project',
|
|
33
|
+
'Run {id}-start --workdir <project>, or create one with /init',
|
|
34
|
+
),
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Add binary checks
|
|
38
|
+
|
|
39
|
+
Use `checkCommand()` for required binaries:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { checkCommand } from '../framework.js';
|
|
43
|
+
|
|
44
|
+
checkCommand('node', {
|
|
45
|
+
hint: 'Install Node.js from https://nodejs.org',
|
|
46
|
+
}),
|
|
47
|
+
checkCommand('pnpm', {
|
|
48
|
+
hint: 'Install pnpm: npm install -g pnpm',
|
|
49
|
+
}),
|
|
50
|
+
checkCommand('ph', {
|
|
51
|
+
hint: 'Install the Powerhouse CLI: npm install -g ph-cli',
|
|
52
|
+
}),
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With version validation:
|
|
56
|
+
```typescript
|
|
57
|
+
checkCommand('node', {
|
|
58
|
+
versionFlag: '--version',
|
|
59
|
+
versionTest: (v) => parseInt(v.replace('v', '')) >= 22,
|
|
60
|
+
hint: 'Node.js >= 22 is required. Update from https://nodejs.org',
|
|
61
|
+
}),
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Add port checks
|
|
65
|
+
|
|
66
|
+
Use `checkPort()` for each port the service binds to:
|
|
67
|
+
|
|
68
|
+
**Static port**:
|
|
69
|
+
```typescript
|
|
70
|
+
import { checkPort } from '../framework.js';
|
|
71
|
+
|
|
72
|
+
checkPort(3000, 'Dev Server'),
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Dynamic port from params**:
|
|
76
|
+
```typescript
|
|
77
|
+
checkPort((ctx) => (ctx.params?.port as number) ?? 3000, 'Dev Server'),
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Multiple ports**:
|
|
81
|
+
```typescript
|
|
82
|
+
checkPort((ctx) => (ctx.params?.connectPort as number) ?? 3000, 'Connect Studio'),
|
|
83
|
+
checkPort((ctx) => (ctx.params?.switchboardPort as number) ?? 4001, 'Switchboard'),
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 4. Order checks correctly
|
|
87
|
+
|
|
88
|
+
Place checks in order from cheapest to most expensive:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
preflight: [
|
|
92
|
+
// 1. Workdir check — pure filesystem, instant
|
|
93
|
+
checkWorkdir(
|
|
94
|
+
(cwd) => fs.existsSync(path.join(cwd, 'package.json')),
|
|
95
|
+
'Not a Node.js project',
|
|
96
|
+
),
|
|
97
|
+
// 2. Binary check — spawns a process, fast
|
|
98
|
+
checkCommand('pnpm', { hint: 'Install pnpm: npm install -g pnpm' }),
|
|
99
|
+
// 3. Port check — network I/O, slowest
|
|
100
|
+
checkPort((ctx) => (ctx.params?.port as number) ?? 3000, 'Dev Server'),
|
|
101
|
+
],
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 5. Write custom preflight checks (if needed)
|
|
105
|
+
|
|
106
|
+
For checks not covered by the built-in factories, write a custom function:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
preflight: [
|
|
110
|
+
async (ctx) => {
|
|
111
|
+
const configPath = path.join(ctx.cwd, '.env');
|
|
112
|
+
if (!fs.existsSync(configPath)) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
message: 'Missing .env file',
|
|
116
|
+
hint: 'Copy .env.example to .env and fill in the values',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return { ok: true };
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Custom checks receive `PreflightContext` with `cwd`, `config`, `params`, and `command`.
|
|
125
|
+
|
|
126
|
+
### 6. Write clear error messages and hints
|
|
127
|
+
|
|
128
|
+
Every failing check should tell the user:
|
|
129
|
+
- **What's wrong**: `'Port 3000 is already in use'`
|
|
130
|
+
- **How to fix it** (hint): `'Stop the other process or use --port <number>'`
|
|
131
|
+
|
|
132
|
+
Good hints save users from having to search for solutions.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Phase 3: Add Project Scanner (Optional)
|
|
2
|
+
|
|
3
|
+
Add project scanning when one service definition can manage multiple project directories. Skip this phase if the service always runs in a single fixed location.
|
|
4
|
+
|
|
5
|
+
## When to Add a Scanner
|
|
6
|
+
|
|
7
|
+
- A dev server that can run in any project folder matching a pattern
|
|
8
|
+
- A reactor service that works with any Powerhouse project
|
|
9
|
+
- Any service where users need to discover eligible project directories
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
### 1. Define the scanner
|
|
14
|
+
|
|
15
|
+
At minimum, implement `isProjectFolder`:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
|
|
21
|
+
export const myService = defineService({
|
|
22
|
+
// ...
|
|
23
|
+
projectScanner: {
|
|
24
|
+
isProjectFolder: (p) => fs.existsSync(path.join(p, 'next.config.ts')),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Add getProjectName (recommended)
|
|
30
|
+
|
|
31
|
+
Extract a display name from the project directory:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
projectScanner: {
|
|
35
|
+
isProjectFolder: (p) => fs.existsSync(path.join(p, 'package.json')),
|
|
36
|
+
getProjectName: (p) => {
|
|
37
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(p, 'package.json'), 'utf-8'));
|
|
38
|
+
return pkg.name ?? path.basename(p);
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 3. Add getProjectConfig (optional)
|
|
44
|
+
|
|
45
|
+
Return project-specific configuration:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
getProjectConfig: (p) => {
|
|
49
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(p, 'package.json'), 'utf-8'));
|
|
50
|
+
return { name: pkg.name, version: pkg.version };
|
|
51
|
+
},
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 4. Add getDocumentLink (optional, Powerhouse projects)
|
|
55
|
+
|
|
56
|
+
Return a document reference for Powerhouse-linked projects:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
getDocumentLink: (p) => {
|
|
60
|
+
const configPath = path.join(p, 'powerhouse.config.json');
|
|
61
|
+
if (!fs.existsSync(configPath)) return undefined;
|
|
62
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
63
|
+
return config.documentId
|
|
64
|
+
? { documentId: config.documentId, documentType: config.documentType }
|
|
65
|
+
: undefined;
|
|
66
|
+
},
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 5. How scanning works
|
|
70
|
+
|
|
71
|
+
When `projectScanner` is defined:
|
|
72
|
+
- The auto-generated `{id}-ls` command becomes available
|
|
73
|
+
- `services.scanProjects(serviceId, rootDir)` performs a breadth-first search
|
|
74
|
+
- The scanner prunes subtrees at foreign project boundaries (`package.json`, `Cargo.toml`, etc.)
|
|
75
|
+
- Returns `ProjectScanResult[]` with `{ name, path, config?, documentId?, documentType? }`
|
|
76
|
+
|
|
77
|
+
### 6. Using scan results
|
|
78
|
+
|
|
79
|
+
Scan results are useful for:
|
|
80
|
+
- Listing projects in the REPL (`/{id}-ls`)
|
|
81
|
+
- Auto-starting services for discovered projects
|
|
82
|
+
- Building project selection menus in interactive mode
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Phase 4: Wire Service Lifecycle Events
|
|
2
|
+
|
|
3
|
+
Handle service state transitions for logging, notifications, and dependent actions.
|
|
4
|
+
|
|
5
|
+
## Steps
|
|
6
|
+
|
|
7
|
+
### 1. Add event handlers in defineCli
|
|
8
|
+
|
|
9
|
+
Service events are wired via the `events` property in `defineCli()`:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { defineCli } from '@powerhousedao/ph-clint';
|
|
13
|
+
|
|
14
|
+
const cli = defineCli({
|
|
15
|
+
// ...
|
|
16
|
+
services: [myService],
|
|
17
|
+
events: {
|
|
18
|
+
'service:pattern-matched': (event, log) => {
|
|
19
|
+
log.info(`${event.name}: matched "${event.patternName}" (${event.remaining} remaining)`);
|
|
20
|
+
},
|
|
21
|
+
'service:ready': (event, log) => {
|
|
22
|
+
log.info(`${event.name} is ready`);
|
|
23
|
+
if (event.endpoints) {
|
|
24
|
+
for (const [name, url] of Object.entries(event.endpoints)) {
|
|
25
|
+
log.info(` ${name}: ${url}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
'service:failed': (event, log) => {
|
|
30
|
+
log.error(`${event.name} failed: ${event.error}`);
|
|
31
|
+
},
|
|
32
|
+
'service:restarting': (event, log) => {
|
|
33
|
+
log.warn(`${event.name} restarting (attempt ${event.attempt}/${event.maxRetries})`);
|
|
34
|
+
},
|
|
35
|
+
'service:stopped': (event, log) => {
|
|
36
|
+
log.info(`${event.name} stopped`);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Understand event payloads
|
|
43
|
+
|
|
44
|
+
Each event handler receives `(event, log)`:
|
|
45
|
+
- `event` — typed payload with `id`, `instanceId`, `name`, and event-specific fields
|
|
46
|
+
- `log` — structured Logger with `debug`, `info`, `warn`, `error` methods
|
|
47
|
+
|
|
48
|
+
**`service:pattern-matched`** — fired progressively as each readiness pattern matches:
|
|
49
|
+
- `event.patternName` — which pattern matched
|
|
50
|
+
- `event.endpoints` — captured endpoints so far
|
|
51
|
+
- `event.endpointTypes` — endpoint types so far
|
|
52
|
+
- `event.remaining` — number of patterns still unmatched
|
|
53
|
+
|
|
54
|
+
**`service:ready`** — fired when ALL patterns have matched:
|
|
55
|
+
- `event.endpoints` — all captured endpoints
|
|
56
|
+
- `event.endpointTypes` — all endpoint types
|
|
57
|
+
|
|
58
|
+
**`service:failed`** — fired on readiness timeout or process crash:
|
|
59
|
+
- `event.error` — error message (includes log tail for diagnosis)
|
|
60
|
+
|
|
61
|
+
**`service:restarting`** — fired when auto-restart triggers:
|
|
62
|
+
- `event.attempt` — current attempt number
|
|
63
|
+
- `event.maxRetries` — maximum attempts configured
|
|
64
|
+
|
|
65
|
+
**`service:stopped`** — fired after explicit stop completes
|
|
66
|
+
|
|
67
|
+
### 3. Common event patterns
|
|
68
|
+
|
|
69
|
+
**Log endpoints on ready**:
|
|
70
|
+
```typescript
|
|
71
|
+
'service:ready': (event, log) => {
|
|
72
|
+
const urls = Object.entries(event.endpoints ?? {})
|
|
73
|
+
.map(([name, url]) => ` ${name}: ${url}`)
|
|
74
|
+
.join('\n');
|
|
75
|
+
log.info(`${event.name} ready\n${urls}`);
|
|
76
|
+
},
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Trigger dependent service startup**:
|
|
80
|
+
```typescript
|
|
81
|
+
'service:ready': async (event, log) => {
|
|
82
|
+
if (event.id === 'backend') {
|
|
83
|
+
log.info('Backend ready, starting frontend...');
|
|
84
|
+
// Frontend can now connect to the backend URL
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Progress feedback during startup**:
|
|
90
|
+
```typescript
|
|
91
|
+
'service:pattern-matched': (event, log) => {
|
|
92
|
+
if (event.remaining > 0) {
|
|
93
|
+
log.info(`${event.name}: ${event.patternName} ready, waiting for ${event.remaining} more...`);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 4. Register the service in defineCli
|
|
99
|
+
|
|
100
|
+
**In a codegen-managed project** (cli.ts has `@clint:begin` markers):
|
|
101
|
+
|
|
102
|
+
Do NOT manually edit `cli.ts` marker regions. Instead:
|
|
103
|
+
|
|
104
|
+
1. Add the service to `project-spec.json`
|
|
105
|
+
2. Run `clint-project-regen` — this regenerates the `@clint:begin services`, `@clint:begin imports`, and `@clint:begin events` marker regions
|
|
106
|
+
3. Run `clint-project-build` to verify TypeScript compiles
|
|
107
|
+
|
|
108
|
+
**In a manually-managed project** (no markers):
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { myService } from './services/my-service.js';
|
|
112
|
+
|
|
113
|
+
const cli = defineCli({
|
|
114
|
+
services: [myService],
|
|
115
|
+
events: { /* ... */ },
|
|
116
|
+
// ...
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This auto-injects the management commands: `{id}-start`, `{id}-stop`, `{id}-restart`, `{id}-ps`, `{id}-logs`, `{id}-manage`, and `{id}-ls` (if projectScanner is defined).
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
## Expected Outcome
|
|
2
|
+
|
|
3
|
+
Service is defined with readiness detection, preflight checks, and lifecycle config. It is registered in `defineCli`, auto-generates management commands (`{id}-start`, `{id}-stop`, etc.), and transitions through starting/ready/failed states correctly. Endpoints are captured and typed. Preflight checks fail fast with clear messages and hints.
|
package/dist/main.js
CHANGED
|
File without changes
|
|
@@ -31,6 +31,11 @@ export const clintProject = defineService({
|
|
|
31
31
|
],
|
|
32
32
|
projectScanner: {
|
|
33
33
|
isProjectFolder: (p) => {
|
|
34
|
+
// Match if a project-spec.json exists (split-layout root)
|
|
35
|
+
const specPath = path.join(p, '.ph', 'ph-clint-cli', 'project-spec.json');
|
|
36
|
+
if (fs.existsSync(specPath))
|
|
37
|
+
return true;
|
|
38
|
+
// Match if package.json has ph-clint as a dependency (flat layout or cli subfolder)
|
|
34
39
|
const pkg = path.join(p, 'package.json');
|
|
35
40
|
if (!fs.existsSync(pkg))
|
|
36
41
|
return false;
|
|
@@ -42,6 +47,22 @@ export const clintProject = defineService({
|
|
|
42
47
|
return false;
|
|
43
48
|
}
|
|
44
49
|
},
|
|
50
|
+
getDocumentLink: (p) => {
|
|
51
|
+
const specPath = path.join(p, '.ph', 'ph-clint-cli', 'project-spec.json');
|
|
52
|
+
if (!fs.existsSync(specPath))
|
|
53
|
+
return undefined;
|
|
54
|
+
try {
|
|
55
|
+
const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
56
|
+
if (spec.documentId) {
|
|
57
|
+
return {
|
|
58
|
+
documentId: spec.documentId,
|
|
59
|
+
documentType: spec.documentType ?? 'powerhouse/ph-clint-project',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { /* noop */ }
|
|
64
|
+
return undefined;
|
|
65
|
+
},
|
|
45
66
|
},
|
|
46
67
|
restart: { enabled: false, maxRetries: 0, delay: 0 },
|
|
47
68
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ExternalSkill } from '../spec/types.js';
|
|
2
|
+
/** On-disk manifest entry (what is currently installed). */
|
|
3
|
+
export interface ManifestEntry {
|
|
4
|
+
name: string;
|
|
5
|
+
githubUrl: string;
|
|
6
|
+
}
|
|
7
|
+
/** Persisted manifest — a simple JSON array. */
|
|
8
|
+
export type SkillsManifest = ManifestEntry[];
|
|
9
|
+
export interface SyncResult {
|
|
10
|
+
added: string[];
|
|
11
|
+
removed: string[];
|
|
12
|
+
unchanged: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface SyncOptions {
|
|
15
|
+
/** Project root (CLI sub-tree when split layout). */
|
|
16
|
+
targetDir: string;
|
|
17
|
+
/** Desired state from the spec. */
|
|
18
|
+
desired: ExternalSkill[];
|
|
19
|
+
/** Optional log callback. */
|
|
20
|
+
log?: (msg: string) => void;
|
|
21
|
+
}
|
|
22
|
+
export declare function readManifest(targetDir: string): Promise<SkillsManifest>;
|
|
23
|
+
/**
|
|
24
|
+
* Compute the diff between desired (spec) and current (manifest) skills.
|
|
25
|
+
*
|
|
26
|
+
* Pure function — no I/O.
|
|
27
|
+
*/
|
|
28
|
+
export declare function diffSkills(desired: ExternalSkill[], current: SkillsManifest): {
|
|
29
|
+
toAdd: ExternalSkill[];
|
|
30
|
+
toRemove: ManifestEntry[];
|
|
31
|
+
unchanged: string[];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Synchronise external skills on disk to match the spec.
|
|
35
|
+
*
|
|
36
|
+
* - Clones missing skills via `git clone --depth 1`.
|
|
37
|
+
* - Removes skills no longer in the spec.
|
|
38
|
+
* - Updates the `.skills-manifest.json`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function syncExternalSkills(opts: SyncOptions): Promise<SyncResult>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External skills synchronisation.
|
|
3
|
+
*
|
|
4
|
+
* Reads the desired external skills from a `ClintProjectSpec`, diffs them
|
|
5
|
+
* against a `.skills-manifest.json` on disk, and runs `git clone` / `rm -rf`
|
|
6
|
+
* to reconcile. Each external skill ends up as a subdirectory of
|
|
7
|
+
* `prompts/skills-ext/<name>/` so the build-skills pipeline picks it up.
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
const MANIFEST_FILE = '.skills-manifest.json';
|
|
13
|
+
const SKILLS_DIR = path.join('prompts', 'skills-ext');
|
|
14
|
+
function manifestPath(targetDir) {
|
|
15
|
+
return path.join(targetDir, SKILLS_DIR, MANIFEST_FILE);
|
|
16
|
+
}
|
|
17
|
+
export async function readManifest(targetDir) {
|
|
18
|
+
try {
|
|
19
|
+
const raw = await fs.readFile(manifestPath(targetDir), 'utf8');
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function writeManifest(targetDir, manifest) {
|
|
27
|
+
const file = manifestPath(targetDir);
|
|
28
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
29
|
+
await fs.writeFile(file, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
30
|
+
}
|
|
31
|
+
function execGit(args, cwd) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const proc = spawn('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
34
|
+
let stderr = '';
|
|
35
|
+
if (proc.stderr) {
|
|
36
|
+
proc.stderr.on('data', (d) => {
|
|
37
|
+
stderr += d.toString();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
proc.on('close', (code) => resolve({ code: code ?? 1, stderr }));
|
|
41
|
+
proc.on('error', (err) => reject(err));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compute the diff between desired (spec) and current (manifest) skills.
|
|
46
|
+
*
|
|
47
|
+
* Pure function — no I/O.
|
|
48
|
+
*/
|
|
49
|
+
export function diffSkills(desired, current) {
|
|
50
|
+
const desiredMap = new Map(desired.map((s) => [s.name, s]));
|
|
51
|
+
const currentMap = new Map(current.map((s) => [s.name, s]));
|
|
52
|
+
const toAdd = [];
|
|
53
|
+
const toRemove = [];
|
|
54
|
+
const unchanged = [];
|
|
55
|
+
for (const [name, skill] of desiredMap) {
|
|
56
|
+
const existing = currentMap.get(name);
|
|
57
|
+
if (!existing) {
|
|
58
|
+
toAdd.push(skill);
|
|
59
|
+
}
|
|
60
|
+
else if (existing.githubUrl !== skill.githubUrl) {
|
|
61
|
+
// URL changed — remove old, add new.
|
|
62
|
+
toRemove.push(existing);
|
|
63
|
+
toAdd.push(skill);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
unchanged.push(name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const [name, entry] of currentMap) {
|
|
70
|
+
if (!desiredMap.has(name)) {
|
|
71
|
+
toRemove.push(entry);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { toAdd, toRemove, unchanged };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Synchronise external skills on disk to match the spec.
|
|
78
|
+
*
|
|
79
|
+
* - Clones missing skills via `git clone --depth 1`.
|
|
80
|
+
* - Removes skills no longer in the spec.
|
|
81
|
+
* - Updates the `.skills-manifest.json`.
|
|
82
|
+
*/
|
|
83
|
+
export async function syncExternalSkills(opts) {
|
|
84
|
+
const { targetDir, desired, log } = opts;
|
|
85
|
+
const skillsDir = path.join(targetDir, SKILLS_DIR);
|
|
86
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
87
|
+
const current = await readManifest(targetDir);
|
|
88
|
+
const { toAdd, toRemove, unchanged } = diffSkills(desired, current);
|
|
89
|
+
const result = { added: [], removed: [], unchanged };
|
|
90
|
+
// Remove skills.
|
|
91
|
+
for (const entry of toRemove) {
|
|
92
|
+
const dir = path.join(skillsDir, entry.name);
|
|
93
|
+
log?.(`Removing skill "${entry.name}"…`);
|
|
94
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
95
|
+
result.removed.push(entry.name);
|
|
96
|
+
}
|
|
97
|
+
// Add skills.
|
|
98
|
+
for (const skill of toAdd) {
|
|
99
|
+
const dir = path.join(skillsDir, skill.name);
|
|
100
|
+
log?.(`Cloning skill "${skill.name}" from ${skill.githubUrl}…`);
|
|
101
|
+
// Ensure the target doesn't exist (re-add after URL change).
|
|
102
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
103
|
+
const { code, stderr } = await execGit(['clone', '--depth', '1', skill.githubUrl, skill.name], skillsDir);
|
|
104
|
+
if (code !== 0) {
|
|
105
|
+
log?.(`Warning: git clone failed for "${skill.name}": ${stderr.trim()}`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Remove .git dir — we don't want a nested repo.
|
|
109
|
+
await fs.rm(path.join(dir, '.git'), { recursive: true, force: true });
|
|
110
|
+
result.added.push(skill.name);
|
|
111
|
+
}
|
|
112
|
+
// Write updated manifest.
|
|
113
|
+
const newManifest = desired.map((s) => ({
|
|
114
|
+
name: s.name,
|
|
115
|
+
githubUrl: s.githubUrl,
|
|
116
|
+
}));
|
|
117
|
+
await writeManifest(targetDir, newManifest);
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FolderOperations } from '@powerhousedao/ph-clint';
|
|
2
|
+
import type { ReactorContext, DocumentRegistry } from '@powerhousedao/ph-clint';
|
|
3
|
+
import type { ClintProjectSpec } from './types.js';
|
|
4
|
+
export interface EnsureSpecDocumentResult {
|
|
5
|
+
docId: string;
|
|
6
|
+
created: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Ensure the spec document exists in the personal drive. Creates one if
|
|
10
|
+
* missing or if the existing `documentId` no longer resolves to a drive node.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ensureSpecDocument<R extends DocumentRegistry = DocumentRegistry>(opts: {
|
|
13
|
+
spec: ClintProjectSpec;
|
|
14
|
+
targetDir: string;
|
|
15
|
+
reactor: ReactorContext<R>;
|
|
16
|
+
folders: FolderOperations;
|
|
17
|
+
}): Promise<EnsureSpecDocumentResult>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure a ph-clint-project spec document exists in the personal drive.
|
|
3
|
+
*
|
|
4
|
+
* Single code path used by:
|
|
5
|
+
* - `clint-project-init` (after scaffolding)
|
|
6
|
+
* - `clint-project-regen` (recover deleted docs)
|
|
7
|
+
* - `spec-change` trigger (auto-link unlinked projects)
|
|
8
|
+
*
|
|
9
|
+
* If the spec already has a `documentId` that exists in the drive, this is
|
|
10
|
+
* a no-op. If the document was deleted (or never created), a new one is
|
|
11
|
+
* created, added to the drive at `specs/{name}`, and the spec JSON on disk
|
|
12
|
+
* is updated with the new `documentId`.
|
|
13
|
+
*/
|
|
14
|
+
import { importSpec } from '@powerhousedao/ph-clint-app/document-models/ph-clint-project';
|
|
15
|
+
import { writeProjectSpec } from './file.js';
|
|
16
|
+
import { specToImportInput } from '../triggers/spec-change.js';
|
|
17
|
+
const DOCUMENT_TYPE = 'powerhouse/ph-clint-project';
|
|
18
|
+
/**
|
|
19
|
+
* Ensure the spec document exists in the personal drive. Creates one if
|
|
20
|
+
* missing or if the existing `documentId` no longer resolves to a drive node.
|
|
21
|
+
*/
|
|
22
|
+
export async function ensureSpecDocument(opts) {
|
|
23
|
+
const { spec, targetDir, reactor, folders } = opts;
|
|
24
|
+
// Check if the document already exists in the drive's folder tree
|
|
25
|
+
if (spec.documentId) {
|
|
26
|
+
const entries = await folders.findByType(DOCUMENT_TYPE);
|
|
27
|
+
const inDrive = entries.some((e) => e.id === spec.documentId);
|
|
28
|
+
if (inDrive)
|
|
29
|
+
return { docId: spec.documentId, created: false };
|
|
30
|
+
}
|
|
31
|
+
let docId;
|
|
32
|
+
if (spec.documentId) {
|
|
33
|
+
try {
|
|
34
|
+
await reactor.client.get(spec.documentId);
|
|
35
|
+
docId = spec.documentId;
|
|
36
|
+
// Orphan recovery: doc exists in reactor but not in drive. Re-attach via
|
|
37
|
+
// addDocument; we can't retrofit CREATE/UPGRADE into the drive's stream.
|
|
38
|
+
await folders.addDocument(docId, `specs/${spec.name}`, spec.name);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
const created = await folders.createDocument(DOCUMENT_TYPE, `specs/${spec.name}`, spec.name);
|
|
42
|
+
docId = created.documentId;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const created = await folders.createDocument(DOCUMENT_TYPE, `specs/${spec.name}`, spec.name);
|
|
47
|
+
docId = created.documentId;
|
|
48
|
+
}
|
|
49
|
+
const importInput = specToImportInput(spec);
|
|
50
|
+
await reactor.client.execute(docId, 'main', [importSpec(importInput)]);
|
|
51
|
+
// Persist the link back to disk
|
|
52
|
+
spec.documentId = docId;
|
|
53
|
+
spec.documentType = DOCUMENT_TYPE;
|
|
54
|
+
await writeProjectSpec(targetDir, spec);
|
|
55
|
+
return { docId, created: true };
|
|
56
|
+
}
|
package/dist/spec/file.d.ts
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function getSpecPath(projectDir: string): string;
|
|
3
|
-
export declare function readProjectSpec(projectDir: string): Promise<ClintProjectSpec | null>;
|
|
4
|
-
export declare function writeProjectSpec(projectDir: string, spec: ClintProjectSpec): Promise<void>;
|
|
1
|
+
export { getSpecPath, readProjectSpec, writeProjectSpec, } from '@powerhousedao/ph-clint-dev/spec/file';
|
package/dist/spec/file.js
CHANGED
|
@@ -1,34 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Persistence for `ClintProjectSpec`.
|
|
3
|
-
* Stored at `{impl-project}/.ph/ph-clint-cli/project-spec.json`.
|
|
4
|
-
*/
|
|
5
|
-
import fs from 'node:fs/promises';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { clintProjectSpecSchema, } from './types.js';
|
|
8
|
-
const SPEC_DIR = path.join('.ph', 'ph-clint-cli');
|
|
9
|
-
const SPEC_FILE = 'project-spec.json';
|
|
10
|
-
export function getSpecPath(projectDir) {
|
|
11
|
-
return path.join(projectDir, SPEC_DIR, SPEC_FILE);
|
|
12
|
-
}
|
|
13
|
-
function isEnoent(err) {
|
|
14
|
-
return (!!err &&
|
|
15
|
-
typeof err === 'object' &&
|
|
16
|
-
'code' in err &&
|
|
17
|
-
err.code === 'ENOENT');
|
|
18
|
-
}
|
|
19
|
-
export async function readProjectSpec(projectDir) {
|
|
20
|
-
try {
|
|
21
|
-
const raw = await fs.readFile(getSpecPath(projectDir), 'utf8');
|
|
22
|
-
return clintProjectSpecSchema.parse(JSON.parse(raw));
|
|
23
|
-
}
|
|
24
|
-
catch (err) {
|
|
25
|
-
if (isEnoent(err))
|
|
26
|
-
return null;
|
|
27
|
-
throw err;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
export async function writeProjectSpec(projectDir, spec) {
|
|
31
|
-
const file = getSpecPath(projectDir);
|
|
32
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
33
|
-
await fs.writeFile(file, JSON.stringify(spec, null, 2) + '\n', 'utf8');
|
|
34
|
-
}
|
|
1
|
+
export { getSpecPath, readProjectSpec, writeProjectSpec, } from '@powerhousedao/ph-clint-dev/spec/file';
|
|
@@ -15,4 +15,7 @@ import { type ClintProjectSpec } from './types.js';
|
|
|
15
15
|
* Returns `null` if the document has no valid package name yet — callers
|
|
16
16
|
* should wait for the user to populate it before triggering codegen.
|
|
17
17
|
*/
|
|
18
|
-
export declare function specFromDocumentState(state: PhClintProjectGlobalState
|
|
18
|
+
export declare function specFromDocumentState(state: PhClintProjectGlobalState, meta?: {
|
|
19
|
+
documentId?: string;
|
|
20
|
+
documentType?: string;
|
|
21
|
+
}): ClintProjectSpec | null;
|