@powerhousedao/ph-clint-cli 0.1.0-dev.4 → 0.1.0-dev.40

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 (90) hide show
  1. package/dist/cli.js +51 -12
  2. package/dist/codegen/actions.d.ts +42 -0
  3. package/dist/codegen/actions.js +372 -0
  4. package/dist/codegen/builders/agent-ts.d.ts +4 -4
  5. package/dist/codegen/builders/agent-ts.js +83 -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 +65 -8
  9. package/dist/codegen/builders/cli-ts.js +65 -24
  10. package/dist/codegen/builders/config-ts.js +11 -15
  11. package/dist/codegen/builders/demo-agent-ts.d.ts +8 -0
  12. package/dist/codegen/builders/demo-agent-ts.js +44 -0
  13. package/dist/codegen/builders/eslint-config-js.d.ts +1 -1
  14. package/dist/codegen/builders/eslint-config-js.js +7 -4
  15. package/dist/codegen/builders/framework-gen-ts.d.ts +4 -5
  16. package/dist/codegen/builders/framework-gen-ts.js +21 -18
  17. package/dist/codegen/builders/framework-ts.js +33 -7
  18. package/dist/codegen/builders/index.d.ts +10 -1
  19. package/dist/codegen/builders/index.js +30 -5
  20. package/dist/codegen/builders/manifest-json.d.ts +6 -0
  21. package/dist/codegen/builders/manifest-json.js +31 -0
  22. package/dist/codegen/builders/mastra-index-ts.d.ts +3 -0
  23. package/dist/codegen/builders/mastra-index-ts.js +47 -5
  24. package/dist/codegen/builders/publish-config-js.d.ts +8 -0
  25. package/dist/codegen/builders/publish-config-js.js +32 -0
  26. package/dist/codegen/builders/publish-config-ts.d.ts +8 -0
  27. package/dist/codegen/builders/publish-config-ts.js +32 -0
  28. package/dist/codegen/builders/readme-md.js +7 -6
  29. package/dist/codegen/builders/root-package-json.d.ts +3 -0
  30. package/dist/codegen/builders/root-package-json.js +26 -11
  31. package/dist/codegen/builders/smoke-test-ts.d.ts +2 -0
  32. package/dist/codegen/builders/smoke-test-ts.js +15 -0
  33. package/dist/codegen/generated.d.ts +17 -0
  34. package/dist/codegen/generated.js +49 -0
  35. package/dist/codegen/index.d.ts +3 -0
  36. package/dist/codegen/index.js +139 -14
  37. package/dist/codegen/scaffold.d.ts +31 -3
  38. package/dist/codegen/scaffold.js +114 -16
  39. package/dist/commands/clint-project-build.js +10 -27
  40. package/dist/commands/clint-project-init.d.ts +30 -6
  41. package/dist/commands/clint-project-init.js +45 -33
  42. package/dist/commands/clint-project-publish.d.ts +1 -0
  43. package/dist/commands/clint-project-publish.js +5 -3
  44. package/dist/commands/clint-project-regen.js +25 -1
  45. package/dist/commands/clint-skills-sync.d.ts +10 -0
  46. package/dist/commands/clint-skills-sync.js +56 -0
  47. package/dist/config.d.ts +3 -5
  48. package/dist/config.js +5 -7
  49. package/dist/framework.d.ts +5 -169
  50. package/dist/framework.gen.d.ts +1 -32
  51. package/dist/framework.js +0 -4
  52. package/{prompts/agent-profiles/AgentBase.md → dist/gen/agent-profiles/PhClintDevAgent.md} +1 -1
  53. package/dist/gen/skills/cli-setup/.cli-docs.md +135 -0
  54. package/dist/gen/skills/cli-setup/SKILL.md +125 -0
  55. package/dist/gen/skills/cli-setup/references/00.assess-cli-needs.md +53 -0
  56. package/dist/gen/skills/cli-setup/references/01.define-config-schema.md +78 -0
  57. package/dist/gen/skills/cli-setup/references/02.wire-cli.md +141 -0
  58. package/dist/gen/skills/cli-setup/references/03.add-entrypoint.md +128 -0
  59. package/dist/gen/skills/cli-setup/references/expected-outcome.md +3 -0
  60. package/dist/gen/skills/command-definition/.cli-docs.md +84 -0
  61. package/dist/gen/skills/command-definition/SKILL.md +99 -0
  62. package/dist/gen/skills/command-definition/references/00.assess-requirements.md +58 -0
  63. package/dist/gen/skills/command-definition/references/01.define-schema.md +72 -0
  64. package/dist/gen/skills/command-definition/references/02.implement-execute.md +118 -0
  65. package/dist/gen/skills/command-definition/references/03.add-prompting.md +54 -0
  66. package/dist/gen/skills/command-definition/references/expected-outcome.md +3 -0
  67. package/dist/gen/skills/service-definition/.cli-docs.md +214 -0
  68. package/dist/gen/skills/service-definition/SKILL.md +148 -0
  69. package/dist/gen/skills/service-definition/references/00.assess-service.md +69 -0
  70. package/dist/gen/skills/service-definition/references/01.define-service.md +166 -0
  71. package/dist/gen/skills/service-definition/references/02.add-preflight.md +132 -0
  72. package/dist/gen/skills/service-definition/references/03.add-project-scanner.md +82 -0
  73. package/dist/gen/skills/service-definition/references/04.wire-events.md +120 -0
  74. package/dist/gen/skills/service-definition/references/expected-outcome.md +3 -0
  75. package/dist/services/clint-project.js +21 -0
  76. package/dist/skills/sync.d.ts +40 -0
  77. package/dist/skills/sync.js +119 -0
  78. package/dist/spec/ensure-document.d.ts +17 -0
  79. package/dist/spec/ensure-document.js +62 -0
  80. package/dist/spec/from-document.d.ts +4 -1
  81. package/dist/spec/from-document.js +18 -8
  82. package/dist/spec/types.d.ts +85 -6
  83. package/dist/spec/types.js +90 -23
  84. package/dist/triggers/publish-trigger.d.ts +10 -0
  85. package/dist/triggers/publish-trigger.js +169 -0
  86. package/dist/triggers/spec-change.d.ts +5 -34
  87. package/dist/triggers/spec-change.js +130 -102
  88. package/package.json +7 -7
  89. package/prompts/skills-ext/.gitkeep +0 -0
  90. package/prompts/skills-tpl/.gitkeep +0 -0
@@ -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
  */
@@ -50,8 +53,8 @@ export async function generateProject(options) {
50
53
  let migrated = false;
51
54
  if (resolvedMode === 'update' &&
52
55
  existingSpec &&
53
- !existingSpec.features.powerhouse.enabled &&
54
- spec.features.powerhouse.enabled) {
56
+ existingSpec.features.powerhouse === 'Disabled' &&
57
+ spec.features.powerhouse !== 'Disabled') {
55
58
  await migrateFlatToSplit({
56
59
  targetDir,
57
60
  spec,
@@ -75,11 +78,12 @@ function resolveMode(requested, specExists) {
75
78
  }
76
79
  /** Build the list of files the new spec would emit, with their content. */
77
80
  function planFiles(spec, targetDir) {
78
- const split = spec.features.powerhouse.enabled;
81
+ const split = spec.features.powerhouse !== 'Disabled';
79
82
  const cliDir = split ? path.join(targetDir, getCliFolderName(spec)) : targetDir;
80
83
  const appDir = split ? path.join(targetDir, getAppFolderName(spec)) : null;
81
84
  const planned = [];
82
- for (const builder of CLI_FILE_BUILDERS) {
85
+ const allBuilders = [...CLI_FILE_BUILDERS, ...getProfileFileBuilders(spec)];
86
+ for (const builder of allBuilders) {
83
87
  const content = builder.build(spec);
84
88
  if (content === null)
85
89
  continue;
@@ -98,6 +102,13 @@ function planFiles(spec, targetDir) {
98
102
  absolutePath: readmeAbs,
99
103
  content: buildReadme(spec),
100
104
  });
105
+ // Publish config — always at the project root (both layouts).
106
+ const publishConfig = path.join(targetDir, 'publish.config.js');
107
+ planned.push({
108
+ relativePath: 'publish.config.js',
109
+ absolutePath: publishConfig,
110
+ content: buildPublishConfigJs(spec),
111
+ });
101
112
  // Split-layout only: root package.json + app placeholder files.
102
113
  if (split && appDir) {
103
114
  const rootPkg = path.join(targetDir, 'package.json');
@@ -147,6 +158,82 @@ function buildAppReadmeContent(spec) {
147
158
  '',
148
159
  ].join('\n');
149
160
  }
161
+ // ---------------------------------------------------------------------------
162
+ // Folder rename handling
163
+ // ---------------------------------------------------------------------------
164
+ async function fileExists(filePath) {
165
+ try {
166
+ await fs.access(filePath);
167
+ return true;
168
+ }
169
+ catch {
170
+ return false;
171
+ }
172
+ }
173
+ /**
174
+ * Detect and execute folder renames when the project name changes.
175
+ *
176
+ * Reads the previous `GeneratedState` to find the old folder names, compares
177
+ * with the new spec's folder names. If they differ:
178
+ * 1. Renames the directories on disk.
179
+ * 2. Rewrites hash-record keys from old → new prefixes.
180
+ *
181
+ * Must run BEFORE `planFiles()` so the reconciliation loop sees the files
182
+ * in their new locations.
183
+ */
184
+ async function handleFolderRename(targetDir, spec, prev, warn) {
185
+ const split = spec.features.powerhouse !== 'Disabled';
186
+ if (!split || !prev || !prev.cliFolderName) {
187
+ return { renamed: false, appInitialized: prev?.appInitialized ?? false };
188
+ }
189
+ const newCliFolder = getCliFolderName(spec);
190
+ const newAppFolder = getAppFolderName(spec);
191
+ const oldCliFolder = prev.cliFolderName;
192
+ const oldAppFolder = prev.appFolderName;
193
+ if (oldCliFolder === newCliFolder && oldAppFolder === newAppFolder) {
194
+ return { renamed: false, appInitialized: prev.appInitialized };
195
+ }
196
+ // Rename CLI folder
197
+ if (oldCliFolder !== newCliFolder) {
198
+ const oldPath = path.join(targetDir, oldCliFolder);
199
+ const newPath = path.join(targetDir, newCliFolder);
200
+ if (await fileExists(oldPath)) {
201
+ if (await fileExists(newPath)) {
202
+ warn(`cannot rename ${oldCliFolder} → ${newCliFolder}: target already exists`);
203
+ return { renamed: false, appInitialized: prev.appInitialized };
204
+ }
205
+ await fs.rename(oldPath, newPath);
206
+ }
207
+ }
208
+ // Rename app folder
209
+ if (oldAppFolder !== newAppFolder) {
210
+ const oldPath = path.join(targetDir, oldAppFolder);
211
+ const newPath = path.join(targetDir, newAppFolder);
212
+ if (await fileExists(oldPath)) {
213
+ if (await fileExists(newPath)) {
214
+ warn(`cannot rename ${oldAppFolder} → ${newAppFolder}: target already exists`);
215
+ return { renamed: false, appInitialized: prev.appInitialized };
216
+ }
217
+ await fs.rename(oldPath, newPath);
218
+ }
219
+ }
220
+ // Rewrite hash keys from old folder prefixes to new ones
221
+ const hashes = await readHashes(targetDir);
222
+ const remapped = {};
223
+ for (const [key, value] of Object.entries(hashes)) {
224
+ let newKey = key;
225
+ if (oldCliFolder !== newCliFolder && key.startsWith(oldCliFolder + '/')) {
226
+ newKey = newCliFolder + key.slice(oldCliFolder.length);
227
+ }
228
+ else if (oldAppFolder !== newAppFolder &&
229
+ key.startsWith(oldAppFolder + '/')) {
230
+ newKey = newAppFolder + key.slice(oldAppFolder.length);
231
+ }
232
+ remapped[newKey] = value;
233
+ }
234
+ await writeHashes(targetDir, remapped);
235
+ return { renamed: true, appInitialized: prev.appInitialized };
236
+ }
150
237
  /** Create mode — empty target dir, write everything fresh. */
151
238
  async function runCreate(options, _warn) {
152
239
  const { targetDir, spec } = options;
@@ -170,7 +257,9 @@ async function runCreate(options, _warn) {
170
257
  relativePath: path.relative(targetDir, getSpecPath(targetDir)),
171
258
  });
172
259
  await writeHashes(targetDir, hashes);
173
- return {
260
+ // App is not yet initialized on create — ph-init runs as a post-gen action.
261
+ await writeGeneratedState(targetDir, generatedStateFromSpec(spec, false));
262
+ const result = {
174
263
  mode: 'create',
175
264
  files,
176
265
  skipped: [],
@@ -178,12 +267,18 @@ async function runCreate(options, _warn) {
178
267
  migrated: false,
179
268
  cliDir,
180
269
  appDir,
270
+ pendingActions: [],
181
271
  };
272
+ result.pendingActions = await collectPostGenActions(result, spec);
273
+ return result;
182
274
  }
183
275
  /** Update mode — reconcile against an existing project. */
184
276
  async function runUpdate(options, warn, migrated) {
185
277
  const { targetDir, spec } = options;
186
278
  const force = !!options.force;
279
+ // --- Folder rename detection ---
280
+ const prev = await readGeneratedState(targetDir);
281
+ const { renamed, appInitialized: appWasInitialized } = await handleFolderRename(targetDir, spec, prev, warn);
187
282
  const { planned, cliDir, appDir } = planFiles(spec, targetDir);
188
283
  const plannedByKey = new Map(planned.map((p) => [p.relativePath, p]));
189
284
  const previous = await readHashes(targetDir);
@@ -195,13 +290,20 @@ async function runUpdate(options, warn, migrated) {
195
290
  for (const p of planned) {
196
291
  if (p.initOnly) {
197
292
  // 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;
293
+ // are emitted only on create. In update mode we never overwrite them —
294
+ // but we DO create them if they don't yet exist on disk (e.g. first
295
+ // spec-change trigger run where no explicit `init` was run before).
296
+ const onDisk = await hashFile(p.absolutePath);
297
+ if (onDisk === null && p.content) {
298
+ await writeFileEnsuringDir(p.absolutePath, p.content);
299
+ next[p.relativePath] = hashContent(p.content);
300
+ files.push({ absolutePath: p.absolutePath, relativePath: p.relativePath });
301
+ }
302
+ else {
303
+ const stored = previous[p.relativePath];
304
+ if (stored !== undefined)
305
+ next[p.relativePath] = stored;
306
+ }
205
307
  continue;
206
308
  }
207
309
  const existing = await hashFile(p.absolutePath);
@@ -280,7 +382,27 @@ async function runUpdate(options, warn, migrated) {
280
382
  }
281
383
  await writeProjectSpec(targetDir, spec);
282
384
  await writeHashes(targetDir, next);
283
- return {
385
+ // Re-patch app package.json if name or scope changed and app is initialized.
386
+ // Add to `files` so collectPostGenActions sees the change and triggers
387
+ // app-install → app-build → cli-install → cli-build.
388
+ const appIsInitialized = appDir
389
+ ? await fileExists(path.join(appDir, 'package.json'))
390
+ : false;
391
+ const nameOrScopeChanged = prev
392
+ ? prev.name !== spec.name || prev.scope !== spec.scope
393
+ : false;
394
+ if (appDir && appIsInitialized && nameOrScopeChanged) {
395
+ const patched = await patchAppPackageName(appDir, spec, warn);
396
+ if (patched) {
397
+ const appFolder = path.basename(appDir);
398
+ files.push({
399
+ absolutePath: path.join(appDir, 'package.json'),
400
+ relativePath: path.join(appFolder, 'package.json'),
401
+ });
402
+ }
403
+ }
404
+ await writeGeneratedState(targetDir, generatedStateFromSpec(spec, appIsInitialized));
405
+ const result = {
284
406
  mode: 'update',
285
407
  files,
286
408
  skipped,
@@ -288,5 +410,8 @@ async function runUpdate(options, warn, migrated) {
288
410
  migrated,
289
411
  cliDir,
290
412
  appDir,
413
+ pendingActions: [],
291
414
  };
415
+ result.pendingActions = await collectPostGenActions(result, spec);
416
+ return result;
292
417
  }
@@ -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,41 @@ 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>;
21
47
  export interface PnpmInstallOptions {
22
48
  /** Directories to run `pnpm install` in, in order. */
23
49
  dirs: string[];
24
50
  log?: (msg: string) => void;
25
51
  /** Override `pnpm` binary name (for tests). */
26
52
  binName?: string;
27
- stdio?: 'inherit' | 'ignore';
53
+ /** Subprocess runner — uses CommandContext.runProcess when available. */
54
+ runProcess?: RunProcess;
28
55
  }
29
56
  export interface PnpmInstallResult {
30
57
  ran: string[];
@@ -32,3 +59,4 @@ export interface PnpmInstallResult {
32
59
  reason?: string;
33
60
  }
34
61
  export declare function runPnpmInstall(options: PnpmInstallOptions): Promise<PnpmInstallResult>;
62
+ export {};
@@ -7,33 +7,118 @@
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
+ }
37
122
  export async function runPnpmInstall(options) {
38
123
  const log = options.log ?? (() => { });
39
124
  const binName = options.binName ?? 'pnpm';
@@ -45,14 +130,27 @@ export async function runPnpmInstall(options) {
45
130
  const ran = [];
46
131
  for (const dir of options.dirs) {
47
132
  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.
133
+ const run = options.runProcess;
134
+ if (run) {
135
+ const result = await run(`${binName} install`, {
136
+ cwd: dir,
137
+ timeout: 300_000,
138
+ });
139
+ if (!result.success) {
140
+ log(`\`${binName} install\` failed in ${dir}. ` +
141
+ 'Retry manually once the underlying issue is resolved.');
142
+ }
143
+ }
144
+ else {
145
+ const { runCommand } = await import('./exec.js');
146
+ const result = await runCommand(binName, ['install'], {
147
+ cwd: dir,
148
+ stdio: 'inherit',
149
+ });
150
+ if (result.exitCode !== 0) {
151
+ log(`\`${binName} install\` exited with code ${result.exitCode} in ${dir}. ` +
152
+ 'Retry manually once the underlying issue is resolved.');
153
+ }
56
154
  }
57
155
  ran.push(dir);
58
156
  }
@@ -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
  },
@@ -35,21 +35,45 @@ export declare const init: import("@powerhousedao/ph-clint").Command<z.ZodObject
35
35
  version: string;
36
36
  description: string;
37
37
  features: {
38
- powerhouse: {
39
- enabled: boolean;
40
- switchboard: boolean;
41
- connect: boolean;
42
- };
38
+ powerhouse: "Disabled" | "Reactor" | "Switchboard" | "Connect";
43
39
  mastra: {
44
40
  enabled: boolean;
41
+ agentId: string | null;
42
+ agentName: string | null;
43
+ agentDescription: string | null;
44
+ agentImage: string | null;
45
+ models: {
46
+ id: string;
47
+ isDefault: boolean;
48
+ }[];
49
+ profiles: {
50
+ id: string;
51
+ title: string;
52
+ content: string;
53
+ }[];
45
54
  };
46
55
  routine: {
47
56
  enabled: boolean;
48
57
  };
49
58
  };
50
- documentTypes: string[];
59
+ packages: {
60
+ id: string;
61
+ packageName: string;
62
+ documentTypes: string[];
63
+ }[];
64
+ externalSkills: {
65
+ id: string;
66
+ name: string;
67
+ githubUrl: string;
68
+ }[];
69
+ deployment: {
70
+ serviceAnnouncement: boolean;
71
+ supportedResources: string[];
72
+ };
51
73
  scope?: string | undefined;
52
74
  bin?: string | undefined;
75
+ documentId?: string | undefined;
76
+ documentType?: string | undefined;
53
77
  };
54
78
  fileCount: number;
55
79
  };