@powerhousedao/ph-clint-cli 0.1.0-dev.5 → 0.1.0-dev.51
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 +50 -9
- package/dist/codegen/actions.d.ts +46 -0
- package/dist/codegen/actions.js +442 -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 +10 -14
- package/dist/codegen/builders/cli-package-json.js +46 -14
- package/dist/codegen/builders/cli-ts.js +87 -25
- 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 +4 -5
- package/dist/codegen/builders/framework-gen-ts.js +26 -18
- package/dist/codegen/builders/framework-ts.js +33 -7
- package/dist/codegen/builders/index.d.ts +10 -1
- package/dist/codegen/builders/index.js +30 -5
- 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/publish-config-ts.d.ts +8 -0
- package/dist/codegen/builders/publish-config-ts.js +32 -0
- package/dist/codegen/builders/readme-md.js +7 -6
- package/dist/codegen/builders/root-package-json.d.ts +3 -0
- package/dist/codegen/builders/root-package-json.js +26 -14
- package/dist/codegen/builders/smoke-test-ts.d.ts +2 -0
- package/dist/codegen/builders/smoke-test-ts.js +15 -0
- package/dist/codegen/generated.d.ts +17 -0
- package/dist/codegen/generated.js +49 -0
- package/dist/codegen/index.d.ts +3 -0
- package/dist/codegen/index.js +154 -23
- package/dist/codegen/scaffold.d.ts +41 -3
- package/dist/codegen/scaffold.js +141 -16
- package/dist/commands/clint-project-build.js +10 -27
- package/dist/commands/clint-project-init.d.ts +34 -9
- package/dist/commands/clint-project-init.js +53 -45
- package/dist/commands/clint-project-publish.d.ts +1 -0
- package/dist/commands/clint-project-publish.js +5 -3
- package/dist/commands/clint-project-regen.js +25 -1
- package/dist/commands/clint-skills-sync.d.ts +10 -0
- package/dist/commands/clint-skills-sync.js +56 -0
- package/dist/framework.d.ts +5 -169
- package/dist/framework.gen.d.ts +1 -32
- package/dist/framework.js +0 -4
- 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/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 +62 -0
- package/dist/spec/from-document.d.ts +4 -1
- package/dist/spec/from-document.js +4 -16
- package/dist/spec/types.d.ts +92 -9
- package/dist/spec/types.js +104 -32
- package/dist/triggers/publish-trigger.d.ts +10 -0
- package/dist/triggers/publish-trigger.js +169 -0
- package/dist/triggers/spec-change.d.ts +5 -34
- package/dist/triggers/spec-change.js +129 -102
- package/package.json +7 -7
- package/prompts/skills-ext/.gitkeep +0 -0
- package/prompts/skills-tpl/.gitkeep +0 -0
package/dist/codegen/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ClintProjectSpec } from '../spec/types.js';
|
|
2
|
+
import { type PostGenAction } from './actions.js';
|
|
2
3
|
export type GenerateMode = 'create' | 'update' | 'auto';
|
|
3
4
|
export interface GenerateProjectOptions {
|
|
4
5
|
/** Absolute path to the project root. */
|
|
@@ -32,6 +33,8 @@ export interface GenerateProjectResult {
|
|
|
32
33
|
migrated: boolean;
|
|
33
34
|
cliDir: string;
|
|
34
35
|
appDir: string | null;
|
|
36
|
+
/** Pending post-generation actions derived from what changed. */
|
|
37
|
+
pendingActions: PostGenAction[];
|
|
35
38
|
}
|
|
36
39
|
/**
|
|
37
40
|
* Generate / reconcile a project at `targetDir` from the given spec.
|
package/dist/codegen/index.js
CHANGED
|
@@ -35,7 +35,10 @@ import { hashContent, hashFile, readHashes, writeHashes, } from './hashes.js';
|
|
|
35
35
|
import { hasMarkers, spliceMarkerRegions } from './markers.js';
|
|
36
36
|
import { writeFileEnsuringDir, isDirEmptyEnough } from './write.js';
|
|
37
37
|
import { migrateFlatToSplit } from './migrate/flat-to-split.js';
|
|
38
|
-
import { CLI_FILE_BUILDERS, buildReadme, buildRootPackageJson, buildAppIndexTs, } from './builders/index.js';
|
|
38
|
+
import { CLI_FILE_BUILDERS, getProfileFileBuilders, buildReadme, buildRootPackageJson, buildPublishConfigJs, buildAppIndexTs, } from './builders/index.js';
|
|
39
|
+
import { collectPostGenActions, } from './actions.js';
|
|
40
|
+
import { readGeneratedState, writeGeneratedState, generatedStateFromSpec, } from './generated.js';
|
|
41
|
+
import { patchAppPackageName } from './scaffold.js';
|
|
39
42
|
/**
|
|
40
43
|
* Generate / reconcile a project at `targetDir` from the given spec.
|
|
41
44
|
*/
|
|
@@ -47,18 +50,24 @@ export async function generateProject(options) {
|
|
|
47
50
|
// Flat → split migration, if needed. Runs BEFORE we compute planned files
|
|
48
51
|
// so the hash record and on-disk state are already in split-layout shape
|
|
49
52
|
// by the time the normal reconciliation runs.
|
|
53
|
+
//
|
|
54
|
+
// Use GeneratedState (what was *actually generated* last time) rather than
|
|
55
|
+
// the on-disk spec to detect the layout flip — when the user edits the spec
|
|
56
|
+
// file before running regen, both existingSpec and spec already say "Reactor"
|
|
57
|
+
// and the old comparison would never trigger.
|
|
50
58
|
let migrated = false;
|
|
51
|
-
if (resolvedMode === 'update' &&
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
if (resolvedMode === 'update' && spec.features.powerhouse !== 'Disabled') {
|
|
60
|
+
const prev = await readGeneratedState(targetDir);
|
|
61
|
+
const wasFlatLayout = prev ? prev.cliFolderName === '' : existingSpec?.features.powerhouse === 'Disabled';
|
|
62
|
+
if (wasFlatLayout) {
|
|
63
|
+
await migrateFlatToSplit({
|
|
64
|
+
targetDir,
|
|
65
|
+
spec,
|
|
66
|
+
force: options.force,
|
|
67
|
+
onWarn: warn,
|
|
68
|
+
});
|
|
69
|
+
migrated = true;
|
|
70
|
+
}
|
|
62
71
|
}
|
|
63
72
|
// Dispatch to the mode-specific flow.
|
|
64
73
|
if (resolvedMode === 'create') {
|
|
@@ -75,11 +84,12 @@ function resolveMode(requested, specExists) {
|
|
|
75
84
|
}
|
|
76
85
|
/** Build the list of files the new spec would emit, with their content. */
|
|
77
86
|
function planFiles(spec, targetDir) {
|
|
78
|
-
const split = spec.features.powerhouse
|
|
87
|
+
const split = spec.features.powerhouse !== 'Disabled';
|
|
79
88
|
const cliDir = split ? path.join(targetDir, getCliFolderName(spec)) : targetDir;
|
|
80
89
|
const appDir = split ? path.join(targetDir, getAppFolderName(spec)) : null;
|
|
81
90
|
const planned = [];
|
|
82
|
-
|
|
91
|
+
const allBuilders = [...CLI_FILE_BUILDERS, ...getProfileFileBuilders(spec)];
|
|
92
|
+
for (const builder of allBuilders) {
|
|
83
93
|
const content = builder.build(spec);
|
|
84
94
|
if (content === null)
|
|
85
95
|
continue;
|
|
@@ -98,6 +108,13 @@ function planFiles(spec, targetDir) {
|
|
|
98
108
|
absolutePath: readmeAbs,
|
|
99
109
|
content: buildReadme(spec),
|
|
100
110
|
});
|
|
111
|
+
// Publish config — always at the project root (both layouts).
|
|
112
|
+
const publishConfig = path.join(targetDir, 'publish.config.js');
|
|
113
|
+
planned.push({
|
|
114
|
+
relativePath: 'publish.config.js',
|
|
115
|
+
absolutePath: publishConfig,
|
|
116
|
+
content: buildPublishConfigJs(spec),
|
|
117
|
+
});
|
|
101
118
|
// Split-layout only: root package.json + app placeholder files.
|
|
102
119
|
if (split && appDir) {
|
|
103
120
|
const rootPkg = path.join(targetDir, 'package.json');
|
|
@@ -147,6 +164,82 @@ function buildAppReadmeContent(spec) {
|
|
|
147
164
|
'',
|
|
148
165
|
].join('\n');
|
|
149
166
|
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Folder rename handling
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
async function fileExists(filePath) {
|
|
171
|
+
try {
|
|
172
|
+
await fs.access(filePath);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Detect and execute folder renames when the project name changes.
|
|
181
|
+
*
|
|
182
|
+
* Reads the previous `GeneratedState` to find the old folder names, compares
|
|
183
|
+
* with the new spec's folder names. If they differ:
|
|
184
|
+
* 1. Renames the directories on disk.
|
|
185
|
+
* 2. Rewrites hash-record keys from old → new prefixes.
|
|
186
|
+
*
|
|
187
|
+
* Must run BEFORE `planFiles()` so the reconciliation loop sees the files
|
|
188
|
+
* in their new locations.
|
|
189
|
+
*/
|
|
190
|
+
async function handleFolderRename(targetDir, spec, prev, warn) {
|
|
191
|
+
const split = spec.features.powerhouse !== 'Disabled';
|
|
192
|
+
if (!split || !prev || !prev.cliFolderName) {
|
|
193
|
+
return { renamed: false, appInitialized: prev?.appInitialized ?? false };
|
|
194
|
+
}
|
|
195
|
+
const newCliFolder = getCliFolderName(spec);
|
|
196
|
+
const newAppFolder = getAppFolderName(spec);
|
|
197
|
+
const oldCliFolder = prev.cliFolderName;
|
|
198
|
+
const oldAppFolder = prev.appFolderName;
|
|
199
|
+
if (oldCliFolder === newCliFolder && oldAppFolder === newAppFolder) {
|
|
200
|
+
return { renamed: false, appInitialized: prev.appInitialized };
|
|
201
|
+
}
|
|
202
|
+
// Rename CLI folder
|
|
203
|
+
if (oldCliFolder !== newCliFolder) {
|
|
204
|
+
const oldPath = path.join(targetDir, oldCliFolder);
|
|
205
|
+
const newPath = path.join(targetDir, newCliFolder);
|
|
206
|
+
if (await fileExists(oldPath)) {
|
|
207
|
+
if (await fileExists(newPath)) {
|
|
208
|
+
warn(`cannot rename ${oldCliFolder} → ${newCliFolder}: target already exists`);
|
|
209
|
+
return { renamed: false, appInitialized: prev.appInitialized };
|
|
210
|
+
}
|
|
211
|
+
await fs.rename(oldPath, newPath);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Rename app folder
|
|
215
|
+
if (oldAppFolder !== newAppFolder) {
|
|
216
|
+
const oldPath = path.join(targetDir, oldAppFolder);
|
|
217
|
+
const newPath = path.join(targetDir, newAppFolder);
|
|
218
|
+
if (await fileExists(oldPath)) {
|
|
219
|
+
if (await fileExists(newPath)) {
|
|
220
|
+
warn(`cannot rename ${oldAppFolder} → ${newAppFolder}: target already exists`);
|
|
221
|
+
return { renamed: false, appInitialized: prev.appInitialized };
|
|
222
|
+
}
|
|
223
|
+
await fs.rename(oldPath, newPath);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Rewrite hash keys from old folder prefixes to new ones
|
|
227
|
+
const hashes = await readHashes(targetDir);
|
|
228
|
+
const remapped = {};
|
|
229
|
+
for (const [key, value] of Object.entries(hashes)) {
|
|
230
|
+
let newKey = key;
|
|
231
|
+
if (oldCliFolder !== newCliFolder && key.startsWith(oldCliFolder + '/')) {
|
|
232
|
+
newKey = newCliFolder + key.slice(oldCliFolder.length);
|
|
233
|
+
}
|
|
234
|
+
else if (oldAppFolder !== newAppFolder &&
|
|
235
|
+
key.startsWith(oldAppFolder + '/')) {
|
|
236
|
+
newKey = newAppFolder + key.slice(oldAppFolder.length);
|
|
237
|
+
}
|
|
238
|
+
remapped[newKey] = value;
|
|
239
|
+
}
|
|
240
|
+
await writeHashes(targetDir, remapped);
|
|
241
|
+
return { renamed: true, appInitialized: prev.appInitialized };
|
|
242
|
+
}
|
|
150
243
|
/** Create mode — empty target dir, write everything fresh. */
|
|
151
244
|
async function runCreate(options, _warn) {
|
|
152
245
|
const { targetDir, spec } = options;
|
|
@@ -170,7 +263,9 @@ async function runCreate(options, _warn) {
|
|
|
170
263
|
relativePath: path.relative(targetDir, getSpecPath(targetDir)),
|
|
171
264
|
});
|
|
172
265
|
await writeHashes(targetDir, hashes);
|
|
173
|
-
|
|
266
|
+
// App is not yet initialized on create — ph-init runs as a post-gen action.
|
|
267
|
+
await writeGeneratedState(targetDir, generatedStateFromSpec(spec, false));
|
|
268
|
+
const result = {
|
|
174
269
|
mode: 'create',
|
|
175
270
|
files,
|
|
176
271
|
skipped: [],
|
|
@@ -178,12 +273,18 @@ async function runCreate(options, _warn) {
|
|
|
178
273
|
migrated: false,
|
|
179
274
|
cliDir,
|
|
180
275
|
appDir,
|
|
276
|
+
pendingActions: [],
|
|
181
277
|
};
|
|
278
|
+
result.pendingActions = await collectPostGenActions(result, spec);
|
|
279
|
+
return result;
|
|
182
280
|
}
|
|
183
281
|
/** Update mode — reconcile against an existing project. */
|
|
184
282
|
async function runUpdate(options, warn, migrated) {
|
|
185
283
|
const { targetDir, spec } = options;
|
|
186
284
|
const force = !!options.force;
|
|
285
|
+
// --- Folder rename detection ---
|
|
286
|
+
const prev = await readGeneratedState(targetDir);
|
|
287
|
+
const { renamed, appInitialized: appWasInitialized } = await handleFolderRename(targetDir, spec, prev, warn);
|
|
187
288
|
const { planned, cliDir, appDir } = planFiles(spec, targetDir);
|
|
188
289
|
const plannedByKey = new Map(planned.map((p) => [p.relativePath, p]));
|
|
189
290
|
const previous = await readHashes(targetDir);
|
|
@@ -195,13 +296,20 @@ async function runUpdate(options, warn, migrated) {
|
|
|
195
296
|
for (const p of planned) {
|
|
196
297
|
if (p.initOnly) {
|
|
197
298
|
// User-owned files (e.g. `src/framework.ts` with its `configSchema`)
|
|
198
|
-
// are emitted only on create. In update mode we never overwrite them
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
next[p.relativePath] =
|
|
299
|
+
// are emitted only on create. In update mode we never overwrite them —
|
|
300
|
+
// but we DO create them if they don't yet exist on disk (e.g. first
|
|
301
|
+
// spec-change trigger run where no explicit `init` was run before).
|
|
302
|
+
const onDisk = await hashFile(p.absolutePath);
|
|
303
|
+
if (onDisk === null && p.content) {
|
|
304
|
+
await writeFileEnsuringDir(p.absolutePath, p.content);
|
|
305
|
+
next[p.relativePath] = hashContent(p.content);
|
|
306
|
+
files.push({ absolutePath: p.absolutePath, relativePath: p.relativePath });
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
const stored = previous[p.relativePath];
|
|
310
|
+
if (stored !== undefined)
|
|
311
|
+
next[p.relativePath] = stored;
|
|
312
|
+
}
|
|
205
313
|
continue;
|
|
206
314
|
}
|
|
207
315
|
const existing = await hashFile(p.absolutePath);
|
|
@@ -280,7 +388,27 @@ async function runUpdate(options, warn, migrated) {
|
|
|
280
388
|
}
|
|
281
389
|
await writeProjectSpec(targetDir, spec);
|
|
282
390
|
await writeHashes(targetDir, next);
|
|
283
|
-
|
|
391
|
+
// Re-patch app package.json if name or scope changed and app is initialized.
|
|
392
|
+
// Add to `files` so collectPostGenActions sees the change and triggers
|
|
393
|
+
// app-install → app-build → cli-install → cli-build.
|
|
394
|
+
const appIsInitialized = appDir
|
|
395
|
+
? await fileExists(path.join(appDir, 'package.json'))
|
|
396
|
+
: false;
|
|
397
|
+
const nameOrScopeChanged = prev
|
|
398
|
+
? prev.name !== spec.name || prev.scope !== spec.scope
|
|
399
|
+
: false;
|
|
400
|
+
if (appDir && appIsInitialized && nameOrScopeChanged) {
|
|
401
|
+
const patched = await patchAppPackageName(appDir, spec, warn);
|
|
402
|
+
if (patched) {
|
|
403
|
+
const appFolder = path.basename(appDir);
|
|
404
|
+
files.push({
|
|
405
|
+
absolutePath: path.join(appDir, 'package.json'),
|
|
406
|
+
relativePath: path.join(appFolder, 'package.json'),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
await writeGeneratedState(targetDir, generatedStateFromSpec(spec, appIsInitialized));
|
|
411
|
+
const result = {
|
|
284
412
|
mode: 'update',
|
|
285
413
|
files,
|
|
286
414
|
skipped,
|
|
@@ -288,5 +416,8 @@ async function runUpdate(options, warn, migrated) {
|
|
|
288
416
|
migrated,
|
|
289
417
|
cliDir,
|
|
290
418
|
appDir,
|
|
419
|
+
pendingActions: [],
|
|
291
420
|
};
|
|
421
|
+
result.pendingActions = await collectPostGenActions(result, spec);
|
|
422
|
+
return result;
|
|
292
423
|
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { ClintProjectSpec } from '../spec/types.js';
|
|
2
|
+
import type { ProcessRunOptions } from '@powerhousedao/ph-clint';
|
|
3
|
+
/**
|
|
4
|
+
* Signature for the subprocess runner — matches CommandContext.runProcess.
|
|
5
|
+
*/
|
|
6
|
+
type RunProcess = (command: string, opts?: Omit<ProcessRunOptions, 'onOutput'>) => Promise<{
|
|
7
|
+
success: boolean;
|
|
8
|
+
output: string;
|
|
9
|
+
}>;
|
|
2
10
|
export interface PhInitOptions {
|
|
3
11
|
/** Absolute path to the project root. */
|
|
4
12
|
targetDir: string;
|
|
@@ -9,22 +17,51 @@ export interface PhInitOptions {
|
|
|
9
17
|
log?: (msg: string) => void;
|
|
10
18
|
/** Override `ph` binary name (for tests). */
|
|
11
19
|
binName?: string;
|
|
12
|
-
/**
|
|
13
|
-
|
|
20
|
+
/** Subprocess runner — uses CommandContext.runProcess when available. */
|
|
21
|
+
runProcess?: RunProcess;
|
|
22
|
+
/** Explicit Powerhouse version to pin `ph init` to (tag or semver). */
|
|
23
|
+
phVersion?: string;
|
|
14
24
|
}
|
|
15
25
|
export interface PhInitResult {
|
|
16
26
|
ran: boolean;
|
|
17
27
|
exitCode?: number;
|
|
18
28
|
reason?: string;
|
|
19
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Detect the installed `ph` CLI version by parsing `ph --version` output.
|
|
32
|
+
* Returns the version string (e.g. '6.0.0-dev.194') or undefined if
|
|
33
|
+
* detection fails.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getPhVersion(binName: string): string | undefined;
|
|
20
36
|
export declare function runPhInit(options: PhInitOptions): Promise<PhInitResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Patch the app `package.json` name to the scoped form (`@scope/name-app`).
|
|
39
|
+
* Called after `ph init` and after name/scope renames.
|
|
40
|
+
*
|
|
41
|
+
* Returns `true` if the file was actually changed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function patchAppPackageName(appDir: string, spec: {
|
|
44
|
+
name: string;
|
|
45
|
+
scope?: string;
|
|
46
|
+
}, log?: (msg: string) => void): Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Run `ph install <packages> --local --pnpm` in the app directory to register
|
|
49
|
+
* external packages in `powerhouse.config.json`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function runPhInstallPackages(options: {
|
|
52
|
+
appDir: string;
|
|
53
|
+
packages: string[];
|
|
54
|
+
log?: (msg: string) => void;
|
|
55
|
+
runProcess?: RunProcess;
|
|
56
|
+
}): Promise<boolean>;
|
|
21
57
|
export interface PnpmInstallOptions {
|
|
22
58
|
/** Directories to run `pnpm install` in, in order. */
|
|
23
59
|
dirs: string[];
|
|
24
60
|
log?: (msg: string) => void;
|
|
25
61
|
/** Override `pnpm` binary name (for tests). */
|
|
26
62
|
binName?: string;
|
|
27
|
-
|
|
63
|
+
/** Subprocess runner — uses CommandContext.runProcess when available. */
|
|
64
|
+
runProcess?: RunProcess;
|
|
28
65
|
}
|
|
29
66
|
export interface PnpmInstallResult {
|
|
30
67
|
ran: string[];
|
|
@@ -32,3 +69,4 @@ export interface PnpmInstallResult {
|
|
|
32
69
|
reason?: string;
|
|
33
70
|
}
|
|
34
71
|
export declare function runPnpmInstall(options: PnpmInstallOptions): Promise<PnpmInstallResult>;
|
|
72
|
+
export {};
|
package/dist/codegen/scaffold.js
CHANGED
|
@@ -7,33 +7,145 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'node:fs/promises';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
|
+
import { hasCommandOnPath } from './exec.js';
|
|
12
|
+
/**
|
|
13
|
+
* Detect the installed `ph` CLI version by parsing `ph --version` output.
|
|
14
|
+
* Returns the version string (e.g. '6.0.0-dev.194') or undefined if
|
|
15
|
+
* detection fails.
|
|
16
|
+
*/
|
|
17
|
+
export function getPhVersion(binName) {
|
|
18
|
+
try {
|
|
19
|
+
const output = execFileSync(binName, ['--version'], {
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
timeout: 5_000,
|
|
22
|
+
});
|
|
23
|
+
const match = output.match(/PH CMD version:\s*(\S+)/);
|
|
24
|
+
return match?.[1];
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
11
30
|
export async function runPhInit(options) {
|
|
12
31
|
const log = options.log ?? (() => { });
|
|
13
32
|
const binName = options.binName ?? 'ph';
|
|
14
33
|
const appFolder = path.basename(options.appDir);
|
|
34
|
+
// Guard: never re-init an already-initialized app directory.
|
|
35
|
+
try {
|
|
36
|
+
await fs.access(path.join(options.appDir, 'package.json'));
|
|
37
|
+
log(`App directory ${appFolder} already initialized — skipping ph init.`);
|
|
38
|
+
return { ran: false, reason: 'already-initialized' };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// No package.json — proceed with init.
|
|
42
|
+
}
|
|
15
43
|
if (!(await hasCommandOnPath(binName))) {
|
|
16
44
|
log(`Skipping \`${binName} init\` — \`${binName}\` is not on PATH. ` +
|
|
17
45
|
`Install @powerhousedao/ph-cli and re-run manually inside ${appFolder}/.`);
|
|
18
46
|
return { ran: false, reason: 'ph-not-on-path' };
|
|
19
47
|
}
|
|
48
|
+
// Pin ph init to the same version as the installed ph CLI, so the
|
|
49
|
+
// generated app uses matching Powerhouse packages.
|
|
50
|
+
const phVersion = options.phVersion ?? getPhVersion(binName);
|
|
51
|
+
const tags = ['dev', 'staging', 'latest'];
|
|
52
|
+
const versionArgs = phVersion
|
|
53
|
+
? tags.includes(phVersion)
|
|
54
|
+
? [`--${phVersion}`]
|
|
55
|
+
: ['--version', phVersion]
|
|
56
|
+
: ['--dev']; // fallback if detection fails
|
|
20
57
|
// `ph init {name}` creates a fresh directory beside the cwd. Our generator
|
|
21
58
|
// placed a `.gitkeep` + README.md placeholder there; clear it out so
|
|
22
59
|
// ph init has a blank slate to work with.
|
|
23
60
|
await fs.rm(options.appDir, { recursive: true, force: true });
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'
|
|
29
|
-
|
|
30
|
-
|
|
61
|
+
const args = ['init', appFolder, ...versionArgs, '--pnpm'];
|
|
62
|
+
log(`Running \`${binName} init ${appFolder}\` (version: ${phVersion ?? 'auto'}) in ${options.targetDir} …`);
|
|
63
|
+
const run = options.runProcess;
|
|
64
|
+
if (run) {
|
|
65
|
+
const result = await run(`${binName} ${args.join(' ')}`, {
|
|
66
|
+
cwd: options.targetDir,
|
|
67
|
+
timeout: 300_000, // ph init + pnpm install can be slow
|
|
68
|
+
});
|
|
69
|
+
const exitCode = result.success ? 0 : 1;
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
log(`\`${binName} init\` failed. ` +
|
|
72
|
+
'You can re-run it manually after resolving the issue.');
|
|
73
|
+
}
|
|
74
|
+
// Patch scoped name
|
|
75
|
+
await patchScopedName(options, appFolder, log);
|
|
76
|
+
return { ran: true, exitCode };
|
|
77
|
+
}
|
|
78
|
+
// Fallback: direct spawn with stdio inherit (for backward compat / tests without context)
|
|
79
|
+
const { runCommand } = await import('./exec.js');
|
|
80
|
+
const result = await runCommand(binName, args, { cwd: options.targetDir, stdio: 'inherit' });
|
|
31
81
|
if (result.exitCode !== 0) {
|
|
32
82
|
log(`\`${binName} init\` exited with code ${result.exitCode}. ` +
|
|
33
83
|
'You can re-run it manually after resolving the issue.');
|
|
34
84
|
}
|
|
85
|
+
await patchScopedName(options, appFolder, log);
|
|
35
86
|
return { ran: true, exitCode: result.exitCode };
|
|
36
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Patch the app `package.json` name to the scoped form (`@scope/name-app`).
|
|
90
|
+
* Called after `ph init` and after name/scope renames.
|
|
91
|
+
*
|
|
92
|
+
* Returns `true` if the file was actually changed.
|
|
93
|
+
*/
|
|
94
|
+
export async function patchAppPackageName(appDir, spec, log) {
|
|
95
|
+
const appFolder = path.basename(appDir);
|
|
96
|
+
const scopedName = spec.scope
|
|
97
|
+
? `${spec.scope}/${appFolder}`
|
|
98
|
+
: appFolder;
|
|
99
|
+
const appPkgJsonPath = path.join(appDir, 'package.json');
|
|
100
|
+
try {
|
|
101
|
+
const raw = await fs.readFile(appPkgJsonPath, 'utf8');
|
|
102
|
+
const pkg = JSON.parse(raw);
|
|
103
|
+
if (pkg.name !== scopedName) {
|
|
104
|
+
pkg.name = scopedName;
|
|
105
|
+
if (spec.scope) {
|
|
106
|
+
pkg.publishConfig = { access: 'public' };
|
|
107
|
+
}
|
|
108
|
+
await fs.writeFile(appPkgJsonPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
109
|
+
log?.(`Patched app package name to ${scopedName}`);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
log?.(`Warning: could not patch app package name: ${err instanceof Error ? err.message : String(err)}`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function patchScopedName(options, _appFolder, log) {
|
|
120
|
+
await patchAppPackageName(options.appDir, options.spec, log);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Run `ph install <packages> --local --pnpm` in the app directory to register
|
|
124
|
+
* external packages in `powerhouse.config.json`.
|
|
125
|
+
*/
|
|
126
|
+
export async function runPhInstallPackages(options) {
|
|
127
|
+
const { appDir, packages, log = () => { }, runProcess } = options;
|
|
128
|
+
const binName = 'ph';
|
|
129
|
+
if (!(await hasCommandOnPath(binName))) {
|
|
130
|
+
log(`Skipping — \`${binName}\` is not on PATH.`);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const args = ['install', ...packages, '--local', '--pnpm'];
|
|
134
|
+
log(`Running \`${binName} install ${packages.join(' ')} --local\` in ${path.basename(appDir)} …`);
|
|
135
|
+
if (runProcess) {
|
|
136
|
+
const result = await runProcess(`${binName} ${args.join(' ')}`, {
|
|
137
|
+
cwd: appDir,
|
|
138
|
+
timeout: 300_000,
|
|
139
|
+
});
|
|
140
|
+
return result.success;
|
|
141
|
+
}
|
|
142
|
+
const { runCommand } = await import('./exec.js');
|
|
143
|
+
const result = await runCommand(binName, args, {
|
|
144
|
+
cwd: appDir,
|
|
145
|
+
stdio: 'inherit',
|
|
146
|
+
});
|
|
147
|
+
return result.exitCode === 0;
|
|
148
|
+
}
|
|
37
149
|
export async function runPnpmInstall(options) {
|
|
38
150
|
const log = options.log ?? (() => { });
|
|
39
151
|
const binName = options.binName ?? 'pnpm';
|
|
@@ -45,14 +157,27 @@ export async function runPnpmInstall(options) {
|
|
|
45
157
|
const ran = [];
|
|
46
158
|
for (const dir of options.dirs) {
|
|
47
159
|
log(`Running \`${binName} install\` in ${dir} …`);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
160
|
+
const run = options.runProcess;
|
|
161
|
+
if (run) {
|
|
162
|
+
const result = await run(`${binName} install`, {
|
|
163
|
+
cwd: dir,
|
|
164
|
+
timeout: 300_000,
|
|
165
|
+
});
|
|
166
|
+
if (!result.success) {
|
|
167
|
+
log(`\`${binName} install\` failed in ${dir}. ` +
|
|
168
|
+
'Retry manually once the underlying issue is resolved.');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const { runCommand } = await import('./exec.js');
|
|
173
|
+
const result = await runCommand(binName, ['install'], {
|
|
174
|
+
cwd: dir,
|
|
175
|
+
stdio: 'inherit',
|
|
176
|
+
});
|
|
177
|
+
if (result.exitCode !== 0) {
|
|
178
|
+
log(`\`${binName} install\` exited with code ${result.exitCode} in ${dir}. ` +
|
|
179
|
+
'Retry manually once the underlying issue is resolved.');
|
|
180
|
+
}
|
|
56
181
|
}
|
|
57
182
|
ran.push(dir);
|
|
58
183
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
2
|
import { defineCommand } from '@powerhousedao/ph-clint';
|
|
4
3
|
import { z } from 'zod';
|
|
5
4
|
import { detectLayout } from '@powerhousedao/ph-clint-dev';
|
|
@@ -7,44 +6,28 @@ const inputSchema = z.object({
|
|
|
7
6
|
dir: z.string().optional().describe('Project root directory (default: cwd)'),
|
|
8
7
|
verbose: z.boolean().default(false),
|
|
9
8
|
});
|
|
10
|
-
function execInDir(dir, cmd, verbose) {
|
|
11
|
-
return new Promise((resolve, reject) => {
|
|
12
|
-
const proc = spawn(cmd, {
|
|
13
|
-
cwd: dir,
|
|
14
|
-
shell: true,
|
|
15
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
16
|
-
});
|
|
17
|
-
let stderr = '';
|
|
18
|
-
if (!verbose && proc.stderr) {
|
|
19
|
-
proc.stderr.on('data', (data) => {
|
|
20
|
-
stderr += data.toString();
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
proc.on('close', (code) => {
|
|
24
|
-
if (code === 0)
|
|
25
|
-
resolve();
|
|
26
|
-
else
|
|
27
|
-
reject(new Error(`Command "${cmd}" failed in ${dir} (exit ${code})${stderr ? `:\n${stderr}` : ''}`));
|
|
28
|
-
});
|
|
29
|
-
proc.on('error', (err) => reject(err));
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
9
|
export const build = defineCommand({
|
|
33
10
|
id: 'clint-project-build',
|
|
34
11
|
description: 'Build all packages in the project',
|
|
35
12
|
inputSchema,
|
|
36
|
-
execute: async (input, { workdir, stdout }) => {
|
|
13
|
+
execute: async (input, { workdir, stdout, runProcess }) => {
|
|
37
14
|
const dir = path.resolve(input.dir ?? workdir);
|
|
38
15
|
const layout = detectLayout(dir);
|
|
39
16
|
if (layout?.type === 'split') {
|
|
40
17
|
stdout(`Building app package (${layout.app})...\n`);
|
|
41
|
-
await
|
|
18
|
+
const appResult = await runProcess('pnpm build', { cwd: layout.app, timeout: 120_000 });
|
|
19
|
+
if (!appResult.success)
|
|
20
|
+
return { text: `App build failed.` };
|
|
42
21
|
stdout(`Building cli package (${layout.cli})...\n`);
|
|
43
|
-
await
|
|
22
|
+
const cliResult = await runProcess('pnpm build', { cwd: layout.cli, timeout: 120_000 });
|
|
23
|
+
if (!cliResult.success)
|
|
24
|
+
return { text: `CLI build failed.` };
|
|
44
25
|
}
|
|
45
26
|
else {
|
|
46
27
|
stdout(`Building project...\n`);
|
|
47
|
-
await
|
|
28
|
+
const result = await runProcess('pnpm build', { cwd: layout?.cli ?? dir, timeout: 120_000 });
|
|
29
|
+
if (!result.success)
|
|
30
|
+
return { text: `Build failed.` };
|
|
48
31
|
}
|
|
49
32
|
return { text: 'Build complete.' };
|
|
50
33
|
},
|
|
@@ -8,7 +8,6 @@ declare const inputSchema: z.ZodObject<{
|
|
|
8
8
|
enableMastra: z.ZodDefault<z.ZodBoolean>;
|
|
9
9
|
enableRoutine: z.ZodDefault<z.ZodBoolean>;
|
|
10
10
|
force: z.ZodDefault<z.ZodBoolean>;
|
|
11
|
-
skipPhInit: z.ZodDefault<z.ZodBoolean>;
|
|
12
11
|
skipInstall: z.ZodDefault<z.ZodBoolean>;
|
|
13
12
|
}, z.core.$strip>;
|
|
14
13
|
type InitInput = z.output<typeof inputSchema>;
|
|
@@ -21,7 +20,6 @@ export declare const init: import("@powerhousedao/ph-clint").Command<z.ZodObject
|
|
|
21
20
|
enableMastra: z.ZodDefault<z.ZodBoolean>;
|
|
22
21
|
enableRoutine: z.ZodDefault<z.ZodBoolean>;
|
|
23
22
|
force: z.ZodDefault<z.ZodBoolean>;
|
|
24
|
-
skipPhInit: z.ZodDefault<z.ZodBoolean>;
|
|
25
23
|
skipInstall: z.ZodDefault<z.ZodBoolean>;
|
|
26
24
|
}, z.core.$strip>, {
|
|
27
25
|
text: string;
|
|
@@ -35,21 +33,48 @@ export declare const init: import("@powerhousedao/ph-clint").Command<z.ZodObject
|
|
|
35
33
|
version: string;
|
|
36
34
|
description: string;
|
|
37
35
|
features: {
|
|
38
|
-
powerhouse:
|
|
39
|
-
enabled: boolean;
|
|
40
|
-
switchboard: boolean;
|
|
41
|
-
connect: boolean;
|
|
42
|
-
};
|
|
36
|
+
powerhouse: "Disabled" | "Reactor" | "Switchboard" | "Connect";
|
|
43
37
|
mastra: {
|
|
44
38
|
enabled: boolean;
|
|
39
|
+
agentId: string | null;
|
|
40
|
+
agentName: string | null;
|
|
41
|
+
agentDescription: string | null;
|
|
42
|
+
agentImage: string | null;
|
|
43
|
+
models: {
|
|
44
|
+
id: string;
|
|
45
|
+
isDefault: boolean;
|
|
46
|
+
}[];
|
|
47
|
+
profiles: {
|
|
48
|
+
id: string;
|
|
49
|
+
title: string;
|
|
50
|
+
content: string;
|
|
51
|
+
}[];
|
|
52
|
+
common: {
|
|
53
|
+
enableChat: boolean;
|
|
54
|
+
};
|
|
45
55
|
};
|
|
46
56
|
routine: {
|
|
47
57
|
enabled: boolean;
|
|
48
58
|
};
|
|
49
59
|
};
|
|
50
|
-
|
|
60
|
+
packages: {
|
|
61
|
+
id: string;
|
|
62
|
+
packageName: string;
|
|
63
|
+
documentTypes: string[];
|
|
64
|
+
version: string | null;
|
|
65
|
+
}[];
|
|
66
|
+
externalSkills: {
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
githubUrl: string;
|
|
70
|
+
}[];
|
|
71
|
+
deployment: {
|
|
72
|
+
proxyEnabled: boolean;
|
|
73
|
+
supportedResources: string[];
|
|
74
|
+
};
|
|
51
75
|
scope?: string | undefined;
|
|
52
|
-
|
|
76
|
+
documentId?: string | undefined;
|
|
77
|
+
documentType?: string | undefined;
|
|
53
78
|
};
|
|
54
79
|
fileCount: number;
|
|
55
80
|
};
|