@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.
Files changed (89) hide show
  1. package/dist/cli.js +50 -9
  2. package/dist/codegen/actions.d.ts +46 -0
  3. package/dist/codegen/actions.js +442 -0
  4. package/dist/codegen/builders/agent-ts.d.ts +4 -4
  5. package/dist/codegen/builders/agent-ts.js +92 -2
  6. package/dist/codegen/builders/app-index-ts.d.ts +3 -10
  7. package/dist/codegen/builders/app-index-ts.js +10 -14
  8. package/dist/codegen/builders/cli-package-json.js +46 -14
  9. package/dist/codegen/builders/cli-ts.js +87 -25
  10. package/dist/codegen/builders/config-ts.d.ts +1 -1
  11. package/dist/codegen/builders/config-ts.js +2 -8
  12. package/dist/codegen/builders/demo-agent-ts.d.ts +8 -0
  13. package/dist/codegen/builders/demo-agent-ts.js +50 -0
  14. package/dist/codegen/builders/eslint-config-js.d.ts +1 -1
  15. package/dist/codegen/builders/eslint-config-js.js +7 -4
  16. package/dist/codegen/builders/framework-gen-ts.d.ts +4 -5
  17. package/dist/codegen/builders/framework-gen-ts.js +26 -18
  18. package/dist/codegen/builders/framework-ts.js +33 -7
  19. package/dist/codegen/builders/index.d.ts +10 -1
  20. package/dist/codegen/builders/index.js +30 -5
  21. package/dist/codegen/builders/manifest-json.d.ts +6 -0
  22. package/dist/codegen/builders/manifest-json.js +31 -0
  23. package/dist/codegen/builders/mastra-index-ts.d.ts +3 -0
  24. package/dist/codegen/builders/mastra-index-ts.js +47 -5
  25. package/dist/codegen/builders/publish-config-js.d.ts +8 -0
  26. package/dist/codegen/builders/publish-config-js.js +33 -0
  27. package/dist/codegen/builders/publish-config-ts.d.ts +8 -0
  28. package/dist/codegen/builders/publish-config-ts.js +32 -0
  29. package/dist/codegen/builders/readme-md.js +7 -6
  30. package/dist/codegen/builders/root-package-json.d.ts +3 -0
  31. package/dist/codegen/builders/root-package-json.js +26 -14
  32. package/dist/codegen/builders/smoke-test-ts.d.ts +2 -0
  33. package/dist/codegen/builders/smoke-test-ts.js +15 -0
  34. package/dist/codegen/generated.d.ts +17 -0
  35. package/dist/codegen/generated.js +49 -0
  36. package/dist/codegen/index.d.ts +3 -0
  37. package/dist/codegen/index.js +154 -23
  38. package/dist/codegen/scaffold.d.ts +41 -3
  39. package/dist/codegen/scaffold.js +141 -16
  40. package/dist/commands/clint-project-build.js +10 -27
  41. package/dist/commands/clint-project-init.d.ts +34 -9
  42. package/dist/commands/clint-project-init.js +53 -45
  43. package/dist/commands/clint-project-publish.d.ts +1 -0
  44. package/dist/commands/clint-project-publish.js +5 -3
  45. package/dist/commands/clint-project-regen.js +25 -1
  46. package/dist/commands/clint-skills-sync.d.ts +10 -0
  47. package/dist/commands/clint-skills-sync.js +56 -0
  48. package/dist/framework.d.ts +5 -169
  49. package/dist/framework.gen.d.ts +1 -32
  50. package/dist/framework.js +0 -4
  51. package/{prompts/agent-profiles/AgentBase.md → dist/gen/agent-profiles/PhClintDevAgent.md} +1 -1
  52. package/dist/gen/skills/cli-setup/.cli-docs.md +135 -0
  53. package/dist/gen/skills/cli-setup/SKILL.md +125 -0
  54. package/dist/gen/skills/cli-setup/references/00.assess-cli-needs.md +53 -0
  55. package/dist/gen/skills/cli-setup/references/01.define-config-schema.md +78 -0
  56. package/dist/gen/skills/cli-setup/references/02.wire-cli.md +141 -0
  57. package/dist/gen/skills/cli-setup/references/03.add-entrypoint.md +128 -0
  58. package/dist/gen/skills/cli-setup/references/expected-outcome.md +3 -0
  59. package/dist/gen/skills/command-definition/.cli-docs.md +84 -0
  60. package/dist/gen/skills/command-definition/SKILL.md +99 -0
  61. package/dist/gen/skills/command-definition/references/00.assess-requirements.md +58 -0
  62. package/dist/gen/skills/command-definition/references/01.define-schema.md +72 -0
  63. package/dist/gen/skills/command-definition/references/02.implement-execute.md +118 -0
  64. package/dist/gen/skills/command-definition/references/03.add-prompting.md +54 -0
  65. package/dist/gen/skills/command-definition/references/expected-outcome.md +3 -0
  66. package/dist/gen/skills/service-definition/.cli-docs.md +214 -0
  67. package/dist/gen/skills/service-definition/SKILL.md +148 -0
  68. package/dist/gen/skills/service-definition/references/00.assess-service.md +69 -0
  69. package/dist/gen/skills/service-definition/references/01.define-service.md +166 -0
  70. package/dist/gen/skills/service-definition/references/02.add-preflight.md +132 -0
  71. package/dist/gen/skills/service-definition/references/03.add-project-scanner.md +82 -0
  72. package/dist/gen/skills/service-definition/references/04.wire-events.md +120 -0
  73. package/dist/gen/skills/service-definition/references/expected-outcome.md +3 -0
  74. package/dist/services/clint-project.js +21 -0
  75. package/dist/skills/sync.d.ts +40 -0
  76. package/dist/skills/sync.js +119 -0
  77. package/dist/spec/ensure-document.d.ts +17 -0
  78. package/dist/spec/ensure-document.js +62 -0
  79. package/dist/spec/from-document.d.ts +4 -1
  80. package/dist/spec/from-document.js +4 -16
  81. package/dist/spec/types.d.ts +92 -9
  82. package/dist/spec/types.js +104 -32
  83. package/dist/triggers/publish-trigger.d.ts +10 -0
  84. package/dist/triggers/publish-trigger.js +169 -0
  85. package/dist/triggers/spec-change.d.ts +5 -34
  86. package/dist/triggers/spec-change.js +129 -102
  87. package/package.json +7 -7
  88. package/prompts/skills-ext/.gitkeep +0 -0
  89. package/prompts/skills-tpl/.gitkeep +0 -0
@@ -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.
@@ -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
- existingSpec &&
53
- !existingSpec.features.powerhouse.enabled &&
54
- spec.features.powerhouse.enabled) {
55
- await migrateFlatToSplit({
56
- targetDir,
57
- spec,
58
- force: options.force,
59
- onWarn: warn,
60
- });
61
- migrated = true;
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.enabled;
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
- for (const builder of CLI_FILE_BUILDERS) {
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
- return {
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
- // and never re-create them if missing — the user may have deliberately
200
- // deleted or inlined them. Keep whatever hash the previous run stored
201
- // so the deletion pass (step 2) still considers them "ours".
202
- const stored = previous[p.relativePath];
203
- if (stored !== undefined)
204
- next[p.relativePath] = stored;
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
- return {
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
- /** Stdio passthrough (tests pass 'ignore'). */
13
- stdio?: 'inherit' | 'ignore';
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
- stdio?: 'inherit' | 'ignore';
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 {};
@@ -7,33 +7,145 @@
7
7
  */
8
8
  import fs from 'node:fs/promises';
9
9
  import path from 'node:path';
10
- import { hasCommandOnPath, runCommand } from './exec.js';
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
- log(`Running \`${binName} init ${appFolder}\` in ${options.targetDir} …`);
25
- const result = await runCommand(binName, [
26
- 'init',
27
- appFolder,
28
- '--pnpm',
29
- '--dev',
30
- ], { cwd: options.targetDir, stdio: options.stdio ?? 'inherit' });
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 result = await runCommand(binName, ['install'], {
49
- cwd: dir,
50
- stdio: options.stdio ?? 'inherit',
51
- });
52
- if (result.exitCode !== 0) {
53
- log(`\`${binName} install\` exited with code ${result.exitCode} in ${dir}. ` +
54
- 'Retry manually once the underlying issue is resolved.');
55
- // Don't abort the whole chain — subsequent dirs may still work.
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 execInDir(layout.app, 'pnpm build', input.verbose);
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 execInDir(layout.cli, 'pnpm build', input.verbose);
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 execInDir(layout?.cli ?? dir, 'pnpm build', input.verbose);
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
- documentTypes: string[];
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
- bin?: string | undefined;
76
+ documentId?: string | undefined;
77
+ documentType?: string | undefined;
53
78
  };
54
79
  fileCount: number;
55
80
  };