@powerhousedao/ph-clint-cli 0.1.0-dev.3 → 0.1.0-dev.30

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 (78) hide show
  1. package/dist/cli.js +31 -12
  2. package/dist/codegen/builders/agent-ts.d.ts +4 -4
  3. package/dist/codegen/builders/agent-ts.js +83 -2
  4. package/dist/codegen/builders/app-index-ts.d.ts +3 -10
  5. package/dist/codegen/builders/app-index-ts.js +10 -14
  6. package/dist/codegen/builders/cli-package-json.js +48 -8
  7. package/dist/codegen/builders/cli-ts.js +63 -22
  8. package/dist/codegen/builders/config-ts.js +11 -15
  9. package/dist/codegen/builders/demo-agent-ts.d.ts +8 -0
  10. package/dist/codegen/builders/demo-agent-ts.js +44 -0
  11. package/dist/codegen/builders/framework-gen-ts.d.ts +4 -5
  12. package/dist/codegen/builders/framework-gen-ts.js +22 -19
  13. package/dist/codegen/builders/framework-ts.js +33 -7
  14. package/dist/codegen/builders/index.d.ts +9 -1
  15. package/dist/codegen/builders/index.js +28 -4
  16. package/dist/codegen/builders/manifest-json.d.ts +6 -0
  17. package/dist/codegen/builders/manifest-json.js +31 -0
  18. package/dist/codegen/builders/mastra-index-ts.d.ts +3 -0
  19. package/dist/codegen/builders/mastra-index-ts.js +47 -5
  20. package/dist/codegen/builders/publish-config-ts.d.ts +6 -0
  21. package/dist/codegen/builders/publish-config-ts.js +21 -0
  22. package/dist/codegen/builders/readme-md.js +8 -7
  23. package/dist/codegen/builders/root-package-json.d.ts +3 -0
  24. package/dist/codegen/builders/root-package-json.js +24 -11
  25. package/dist/codegen/index.js +26 -12
  26. package/dist/codegen/scaffold.d.ts +21 -3
  27. package/dist/codegen/scaffold.js +90 -16
  28. package/dist/commands/clint-project-build.js +10 -27
  29. package/dist/commands/clint-project-init.d.ts +30 -6
  30. package/dist/commands/clint-project-init.js +45 -9
  31. package/dist/commands/clint-project-publish.d.ts +1 -0
  32. package/dist/commands/clint-project-publish.js +5 -3
  33. package/dist/commands/clint-project-regen.js +2 -2
  34. package/dist/commands/clint-skills-sync.d.ts +10 -0
  35. package/dist/commands/clint-skills-sync.js +56 -0
  36. package/dist/config.d.ts +3 -5
  37. package/dist/config.js +5 -7
  38. package/dist/framework.d.ts +5 -169
  39. package/dist/framework.gen.d.ts +1 -32
  40. package/dist/framework.gen.js +1 -1
  41. package/dist/framework.js +0 -4
  42. package/{prompts/agent-profiles/AgentBase.md → dist/gen/agent-profiles/PhClintDevAgent.md} +1 -1
  43. package/dist/gen/skills/cli-setup/.cli-docs.md +135 -0
  44. package/dist/gen/skills/cli-setup/SKILL.md +125 -0
  45. package/dist/gen/skills/cli-setup/references/00.assess-cli-needs.md +53 -0
  46. package/dist/gen/skills/cli-setup/references/01.define-config-schema.md +78 -0
  47. package/dist/gen/skills/cli-setup/references/02.wire-cli.md +141 -0
  48. package/dist/gen/skills/cli-setup/references/03.add-entrypoint.md +128 -0
  49. package/dist/gen/skills/cli-setup/references/expected-outcome.md +3 -0
  50. package/dist/gen/skills/command-definition/.cli-docs.md +84 -0
  51. package/dist/gen/skills/command-definition/SKILL.md +99 -0
  52. package/dist/gen/skills/command-definition/references/00.assess-requirements.md +58 -0
  53. package/dist/gen/skills/command-definition/references/01.define-schema.md +72 -0
  54. package/dist/gen/skills/command-definition/references/02.implement-execute.md +118 -0
  55. package/dist/gen/skills/command-definition/references/03.add-prompting.md +54 -0
  56. package/dist/gen/skills/command-definition/references/expected-outcome.md +3 -0
  57. package/dist/gen/skills/service-definition/.cli-docs.md +214 -0
  58. package/dist/gen/skills/service-definition/SKILL.md +148 -0
  59. package/dist/gen/skills/service-definition/references/00.assess-service.md +69 -0
  60. package/dist/gen/skills/service-definition/references/01.define-service.md +166 -0
  61. package/dist/gen/skills/service-definition/references/02.add-preflight.md +132 -0
  62. package/dist/gen/skills/service-definition/references/03.add-project-scanner.md +82 -0
  63. package/dist/gen/skills/service-definition/references/04.wire-events.md +120 -0
  64. package/dist/gen/skills/service-definition/references/expected-outcome.md +3 -0
  65. package/dist/services/clint-project.js +21 -0
  66. package/dist/skills/sync.d.ts +40 -0
  67. package/dist/skills/sync.js +119 -0
  68. package/dist/spec/from-document.d.ts +4 -1
  69. package/dist/spec/from-document.js +18 -8
  70. package/dist/spec/types.d.ts +85 -6
  71. package/dist/spec/types.js +90 -23
  72. package/dist/triggers/publish-trigger.d.ts +10 -0
  73. package/dist/triggers/publish-trigger.js +169 -0
  74. package/dist/triggers/spec-change.d.ts +5 -34
  75. package/dist/triggers/spec-change.js +175 -99
  76. package/package.json +7 -7
  77. package/prompts/skills-ext/.gitkeep +0 -0
  78. package/prompts/skills-tpl/.gitkeep +0 -0
@@ -4,47 +4,50 @@
4
4
  * `defineRegistry([...] as const)` call that describes which document models
5
5
  * are bound into `framework.ts`'s typed factories.
6
6
  *
7
- * Kept deliberately small: this file exports only `registry` and the
8
- * `Registry` type. The `createTypes` call lives in `framework.ts` (the
9
- * user-owned half) so `configSchema` edits stay local and we avoid an import
10
- * cycle between the two files.
7
+ * Iterates all packages' document types. The app package's modules are
8
+ * imported from the local `{name}-app` package; external packages' modules
9
+ * are imported from their npm package name.
11
10
  *
12
11
  * Emitted only when Powerhouse is enabled — flat-layout impls have no
13
12
  * reactor package to import modules from and bind `createTypes` directly in
14
- * `framework.ts`. When Powerhouse is on but no `documentTypes` are declared,
13
+ * `framework.ts`. When Powerhouse is on but no document types are declared,
15
14
  * we still emit this file with an empty `defineRegistry([])` so impl code's
16
15
  * `import { registry } from './framework.gen.js'` keeps resolving across
17
16
  * regens.
18
17
  */
19
- import { getDocumentTypeModuleName, } from '../../spec/types.js';
20
- /** Local package name of the reactor package for this project. */
21
- function appPackageName(spec) {
22
- return `${spec.name}-app`;
23
- }
18
+ import { getAppPackageName, getDocumentTypeModuleName, phAtLeast, } from '../../spec/types.js';
24
19
  export function buildFrameworkGenTs(spec) {
25
- if (!spec.features.powerhouse.enabled)
20
+ if (!phAtLeast(spec.features.powerhouse, 'Reactor'))
26
21
  return null;
27
- const pkg = appPackageName(spec);
28
- const entries = spec.documentTypes.map((docType) => ({
29
- docType,
30
- name: getDocumentTypeModuleName(docType),
31
- }));
22
+ const appPkg = getAppPackageName(spec);
23
+ // Collect all (docType, moduleName, importFrom) tuples across packages.
24
+ const entries = [];
25
+ for (const pkg of spec.packages) {
26
+ const importFrom = pkg.packageName === appPkg ? appPkg : pkg.packageName;
27
+ for (const docType of pkg.documentTypes) {
28
+ entries.push({
29
+ docType,
30
+ name: getDocumentTypeModuleName(docType),
31
+ importFrom,
32
+ });
33
+ }
34
+ }
32
35
  const lines = [];
33
36
  lines.push('/**');
34
37
  lines.push(' * THIS FILE IS GENERATED BY ph-clint-cli. DO NOT EDIT.');
35
38
  lines.push(' *');
36
- lines.push(' * Regenerated on every `ph-clint regen` and on every spec-change');
39
+ lines.push(' * Regenerated on every `ph-clint clint-project-regen` and on every spec-change');
37
40
  lines.push(' * tick. Put custom bindings in `framework.ts`, not here — edits to');
38
41
  lines.push(' * this file will be overwritten.');
39
42
  lines.push(' */');
40
43
  lines.push(`import { defineRegistry } from '@powerhousedao/ph-clint';`);
41
44
  for (const e of entries) {
42
- lines.push(`import { ${e.name} } from '${pkg}';`);
45
+ lines.push(`import { ${e.name} } from '${e.importFrom}';`);
43
46
  }
44
47
  lines.push('');
45
48
  lines.push('/**');
46
49
  lines.push(' * Typed document registry derived from the reactor package\'s document models.');
47
- lines.push(' * Regenerated whenever the spec\'s `documentTypes` list changes.');
50
+ lines.push(' * Regenerated whenever the spec\'s packages or document types change.');
48
51
  lines.push(' */');
49
52
  if (entries.length === 0) {
50
53
  lines.push('export const registry = defineRegistry([] as const);');
@@ -1,6 +1,26 @@
1
+ /**
2
+ * Builds `src/framework.ts` — the user-owned half of the typed-binding split.
3
+ *
4
+ * Emitted **only on create**. Once the file exists, codegen never overwrites
5
+ * it: users are expected to extend `configSchema` and `secretsSchema` here,
6
+ * and those edits must survive regens.
7
+ *
8
+ * Behavior differs by layout:
9
+ *
10
+ * - **Powerhouse enabled** — this file declares `configSchema`/`secretsSchema`
11
+ * and re-exports `registry`, `defineCommand`, `defineTrigger`,
12
+ * `defineService`, and `createDocumentChangeTrigger` from the companion
13
+ * `framework.gen.ts`. Adding a document type to the spec regenerates
14
+ * `framework.gen.ts`; this file keeps resolving its re-exports unchanged.
15
+ *
16
+ * - **Powerhouse disabled** — no reactor package exists to register modules
17
+ * against, so this file binds `createTypes({ configSchema })` directly and
18
+ * exports the typed factories from here. No `framework.gen.ts` is emitted.
19
+ */
20
+ import { phAtLeast } from '../../spec/types.js';
1
21
  export function buildFrameworkTs(spec) {
2
22
  const { mastra, powerhouse } = spec.features;
3
- const hasGen = powerhouse.enabled;
23
+ const hasGen = phAtLeast(powerhouse, 'Reactor');
4
24
  const lines = [];
5
25
  lines.push('/**');
6
26
  lines.push(' * Per-CLI typed framework binding.');
@@ -22,24 +42,30 @@ export function buildFrameworkTs(spec) {
22
42
  }
23
43
  lines.push('');
24
44
  lines.push('export const configSchema = z.object({');
45
+ lines.push(' // @clint:begin framework-config');
25
46
  if (mastra.enabled) {
26
- lines.push(" model: z.string().default('anthropic/claude-haiku-4-5').describe('LLM model to use'),");
47
+ const defaultModel = mastra.models.find(m => m.isDefault)?.id ?? 'anthropic/claude-sonnet-4-5';
48
+ lines.push(` model: z.string().default('${defaultModel}').describe('LLM model to use'),`, ` agentLogging: z.boolean().default(false).describe('Enable agent conversation logging'),`);
27
49
  }
28
- // Ports are derived from CLI name by resolveReactorDefaults() — no config fields needed.
29
- // Override via switchboard.port / connect.port in configureReactor() if required.
30
- lines.push(' // Add your own config fields here — this file survives regens.');
50
+ lines.push(' // @clint:end framework-config');
31
51
  lines.push('});');
32
52
  lines.push('');
33
53
  lines.push('export const secretsSchema = z.object({');
54
+ lines.push(' // @clint:begin framework-secrets');
34
55
  if (mastra.enabled) {
35
- lines.push(" apiKey: z.string().optional().describe('LLM API key'),");
56
+ const providers = [...new Set(mastra.models.map(m => m.id.split(/[:/]/)[0]))];
57
+ for (const provider of providers) {
58
+ const field = `${provider}ApiKey`;
59
+ lines.push(` ${field}: z.string().optional().describe('${provider} API key'),`);
60
+ }
36
61
  }
62
+ lines.push(' // @clint:end framework-secrets');
37
63
  lines.push('});');
38
64
  lines.push('');
39
65
  lines.push('export type Config = z.infer<typeof configSchema> &');
40
66
  lines.push(' z.infer<typeof secretsSchema>;');
41
67
  lines.push('');
42
- lines.push('const fullConfigSchema = configSchema.merge(secretsSchema);');
68
+ lines.push('const fullConfigSchema = configSchema.extend(secretsSchema.shape);');
43
69
  lines.push('');
44
70
  if (hasGen) {
45
71
  lines.push('export const {');
@@ -17,8 +17,10 @@ import { buildMastraIndexTs } from './mastra-index-ts.js';
17
17
  import { buildBuildSkillsScript } from './build-skills-ts.js';
18
18
  import { buildAgentBaseMd } from './agent-base-md.js';
19
19
  import { buildAgentTs } from './agent-ts.js';
20
+ import { buildDemoAgentTs } from './demo-agent-ts.js';
20
21
  import { buildFrameworkGenTs } from './framework-gen-ts.js';
21
22
  import { buildFrameworkTs } from './framework-ts.js';
23
+ import { buildManifestJson } from './manifest-json.js';
22
24
  import { type ClintProjectSpec } from '../../spec/types.js';
23
25
  /**
24
26
  * A spec-driven file builder. Returns `null` from `build` to indicate the
@@ -36,8 +38,14 @@ export interface FileBuilder {
36
38
  */
37
39
  initOnly?: boolean;
38
40
  }
41
+ /**
42
+ * Generate dynamic file builders for agent profile markdown files.
43
+ * One file per profile in `prompts/agent-profiles/{id}.md`.
44
+ */
45
+ export declare function getProfileFileBuilders(spec: ClintProjectSpec): FileBuilder[];
39
46
  export declare const CLI_FILE_BUILDERS: FileBuilder[];
40
- export { buildCliPackageJson, buildCliTs, buildConfigTs, buildMainTs, buildTsconfigJson, buildJestConfigJs, buildEslintConfigJs, buildGitignore, buildMastraIndexTs, buildBuildSkillsScript, buildAgentBaseMd, buildAgentTs, buildFrameworkGenTs, buildFrameworkTs, };
47
+ export { buildCliPackageJson, buildCliTs, buildConfigTs, buildMainTs, buildTsconfigJson, buildJestConfigJs, buildEslintConfigJs, buildGitignore, buildMastraIndexTs, buildBuildSkillsScript, buildAgentBaseMd, buildAgentTs, buildDemoAgentTs, buildFrameworkGenTs, buildFrameworkTs, buildManifestJson, };
41
48
  export { buildRootPackageJson } from './root-package-json.js';
49
+ export { buildPublishConfigTs } from './publish-config-ts.js';
42
50
  export { buildReadme } from './readme-md.js';
43
51
  export { buildAppIndexTs } from './app-index-ts.js';
@@ -17,9 +17,25 @@ import { buildMastraIndexTs } from './mastra-index-ts.js';
17
17
  import { buildBuildSkillsScript } from './build-skills-ts.js';
18
18
  import { buildAgentBaseMd } from './agent-base-md.js';
19
19
  import { buildAgentTs } from './agent-ts.js';
20
+ import { buildDemoAgentTs } from './demo-agent-ts.js';
20
21
  import { buildFrameworkGenTs } from './framework-gen-ts.js';
21
22
  import { buildFrameworkTs } from './framework-ts.js';
23
+ import { buildManifestJson } from './manifest-json.js';
24
+ /**
25
+ * Generate dynamic file builders for agent profile markdown files.
26
+ * One file per profile in `prompts/agent-profiles/{id}.md`.
27
+ */
28
+ export function getProfileFileBuilders(spec) {
29
+ const { mastra } = spec.features;
30
+ if (!mastra.enabled || mastra.profiles.length === 0)
31
+ return [];
32
+ return mastra.profiles.map((profile) => ({
33
+ relativePath: `prompts/agent-profiles/${profile.id}.md`,
34
+ build: () => profile.content + '\n',
35
+ }));
36
+ }
22
37
  export const CLI_FILE_BUILDERS = [
38
+ { relativePath: 'powerhouse.manifest.json', build: buildManifestJson },
23
39
  { relativePath: 'package.json', build: buildCliPackageJson },
24
40
  { relativePath: 'tsconfig.json', build: () => buildTsconfigJson() },
25
41
  { relativePath: 'jest.config.js', build: () => buildJestConfigJs() },
@@ -28,17 +44,24 @@ export const CLI_FILE_BUILDERS = [
28
44
  { relativePath: 'src/main.ts', build: () => buildMainTs() },
29
45
  { relativePath: 'src/cli.ts', build: buildCliTs },
30
46
  { relativePath: 'src/config.ts', build: buildConfigTs },
31
- // User-owned; init-only. Holds `configSchema` + `secretsSchema` edits.
32
- { relativePath: 'src/framework.ts', build: buildFrameworkTs, initOnly: true },
47
+ // User-owned outside markers; generated config/secrets inside markers.
48
+ { relativePath: 'src/framework.ts', build: buildFrameworkTs },
33
49
  // Machine-owned; regenerated on every run. Null when no document types.
34
50
  { relativePath: 'src/framework.gen.ts', build: buildFrameworkGenTs },
35
51
  { relativePath: 'src/mastra/index.ts', build: buildMastraIndexTs },
36
52
  { relativePath: 'scripts/build-skills.ts', build: () => buildBuildSkillsScript() },
37
- { relativePath: 'prompts/agent-profiles/AgentBase.md', build: buildAgentBaseMd },
53
+ {
54
+ relativePath: 'prompts/agent-profiles/AgentBase.md',
55
+ build: (spec) => (spec.features.mastra.profiles.length > 0 ? null : buildAgentBaseMd(spec)),
56
+ },
38
57
  {
39
58
  relativePath: 'src/agents/agent.ts',
40
59
  build: (spec) => (spec.features.mastra.enabled ? buildAgentTs(spec) : null),
41
60
  },
61
+ {
62
+ relativePath: 'src/agents/demo-agent.ts',
63
+ build: buildDemoAgentTs,
64
+ },
42
65
  // Gitkeep placeholders so directories are tracked in git when otherwise empty.
43
66
  { relativePath: 'src/commands/.gitkeep', build: () => '' },
44
67
  { relativePath: 'src/services/.gitkeep', build: () => '' },
@@ -54,7 +77,8 @@ export const CLI_FILE_BUILDERS = [
54
77
  { relativePath: 'prompts/skills-ext/.gitkeep', build: () => '' },
55
78
  { relativePath: 'tests/.gitkeep', build: () => '' },
56
79
  ];
57
- export { buildCliPackageJson, buildCliTs, buildConfigTs, buildMainTs, buildTsconfigJson, buildJestConfigJs, buildEslintConfigJs, buildGitignore, buildMastraIndexTs, buildBuildSkillsScript, buildAgentBaseMd, buildAgentTs, buildFrameworkGenTs, buildFrameworkTs, };
80
+ export { buildCliPackageJson, buildCliTs, buildConfigTs, buildMainTs, buildTsconfigJson, buildJestConfigJs, buildEslintConfigJs, buildGitignore, buildMastraIndexTs, buildBuildSkillsScript, buildAgentBaseMd, buildAgentTs, buildDemoAgentTs, buildFrameworkGenTs, buildFrameworkTs, buildManifestJson, };
58
81
  export { buildRootPackageJson } from './root-package-json.js';
82
+ export { buildPublishConfigTs } from './publish-config-ts.js';
59
83
  export { buildReadme } from './readme-md.js';
60
84
  export { buildAppIndexTs } from './app-index-ts.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Builds `powerhouse.manifest.json` — a machine-readable manifest describing
3
+ * the CLI project's capabilities for service discovery and deployment.
4
+ */
5
+ import { type ClintProjectSpec } from '../../spec/types.js';
6
+ export declare function buildManifestJson(spec: ClintProjectSpec): string;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Builds `powerhouse.manifest.json` — a machine-readable manifest describing
3
+ * the CLI project's capabilities for service discovery and deployment.
4
+ */
5
+ import { getBinName, getAppPackageName, phAtLeast, } from '../../spec/types.js';
6
+ export function buildManifestJson(spec) {
7
+ const { mastra, powerhouse } = spec.features;
8
+ const agent = mastra.enabled
9
+ ? {
10
+ id: mastra.agentId ?? spec.name,
11
+ name: mastra.agentName ?? spec.name,
12
+ description: mastra.agentDescription ?? null,
13
+ image: mastra.agentImage ?? null,
14
+ models: mastra.models.map((m) => ({ id: m.id, default: m.isDefault })),
15
+ }
16
+ : false;
17
+ const ph = phAtLeast(powerhouse, 'Reactor')
18
+ ? {
19
+ support: powerhouse,
20
+ package: getAppPackageName(spec),
21
+ }
22
+ : false;
23
+ const manifest = {
24
+ type: 'clint-project',
25
+ features: { agent, powerhouse: ph },
26
+ serviceCommand: getBinName(spec),
27
+ serviceAnnouncement: spec.deployment.serviceAnnouncement,
28
+ supportedResources: spec.deployment.supportedResources,
29
+ };
30
+ return JSON.stringify(manifest, null, 2) + '\n';
31
+ }
@@ -3,6 +3,9 @@
3
3
  *
4
4
  * When Mastra is off, emit a no-op placeholder so the directory still exists
5
5
  * and `mastra dev` fails predictably rather than importing partial code.
6
+ *
7
+ * When Mastra is on with full agent config, bootstrap the CLI and re-export
8
+ * the configured Mastra Agent via `cli.bootstrap()`.
6
9
  */
7
10
  import { type ClintProjectSpec } from '../../spec/types.js';
8
11
  export declare function buildMastraIndexTs(spec: ClintProjectSpec): string;
@@ -1,28 +1,70 @@
1
+ /**
2
+ * Derive the `{PROVIDER}_API_KEY` env var name from a provider prefix
3
+ * (e.g. `anthropic` → `ANTHROPIC_API_KEY`).
4
+ */
5
+ function getApiKeyEnvVar(provider) {
6
+ return `${provider.toUpperCase()}_API_KEY`;
7
+ }
1
8
  export function buildMastraIndexTs(spec) {
2
9
  const { mastra } = spec.features;
3
10
  if (!mastra.enabled) {
4
11
  return [
12
+ '// @clint:begin mastra-index',
5
13
  '/**',
6
14
  ' * Mastra Studio entry point — placeholder. Enable the `mastra` feature to',
7
15
  ' * populate this file. Until then `mastra:*` scripts are effectively no-ops.',
8
16
  ' */',
9
- '// @clint:begin mastra-index',
10
17
  'export {};',
11
18
  '// @clint:end mastra-index',
12
19
  '',
13
20
  ].join('\n');
14
21
  }
15
- // When Mastra is enabled but we still emit the demo `AgentProvider` shim
16
- // (not a real `@mastra/core` Agent), there is no `mastra` export yet —
17
- // `mastra dev` will surface a helpful message once you wire a real Agent.
22
+ // Full agent config available — emit bootstrap-based Mastra instance
23
+ if (mastra.agentId && mastra.models.length > 0) {
24
+ const providers = [...new Set(mastra.models.map(m => m.id.split(/[:/]/)[0]))];
25
+ const lines = [
26
+ '// @clint:begin mastra-index',
27
+ '/**',
28
+ ' * Mastra Studio entry point for `mastra dev` / `mastra build` / `mastra start`.',
29
+ ' *',
30
+ ' * Bootstraps the CLI to get the configured agent, then exports a Mastra',
31
+ ' * instance for Studio integration.',
32
+ ' */',
33
+ "import { Mastra } from '@mastra/core/mastra';",
34
+ "import type { Agent } from '@mastra/core/agent';",
35
+ "import path from 'node:path';",
36
+ "import { cli } from '../cli.js';",
37
+ "import { CLI_ROOT } from '../config.js';",
38
+ '',
39
+ "// Under `mastra dev`, CLI_ROOT points to .mastra/ (bundler output).",
40
+ '// The actual project root is its parent.',
41
+ "const projectRoot = path.basename(CLI_ROOT) === '.mastra'",
42
+ ' ? path.dirname(CLI_ROOT)',
43
+ ' : CLI_ROOT;',
44
+ '',
45
+ 'const rt = await cli.bootstrap({ workdir: projectRoot });',
46
+ '',
47
+ ];
48
+ // API key env bridge for Studio
49
+ lines.push(`// Workaround: Mastra Studio checks process.env for API keys directly.`, `// Bridge the ph-clint resolved keys so Studio's provider detection works.`);
50
+ for (const provider of providers) {
51
+ const field = `${provider}ApiKey`;
52
+ const envVar = getApiKeyEnvVar(provider);
53
+ lines.push(`{`, ` const key = rt.config.${field} as string | undefined;`, ` if (key && !process.env.${envVar}) process.env.${envVar} = key;`, `}`);
54
+ }
55
+ lines.push('');
56
+ lines.push('const mastraAgent = await rt.mastraAgent as Agent | undefined;', '', 'export const mastra = new Mastra({', ' agents: mastraAgent ? { [mastraAgent.id]: mastraAgent } : {},', '});', '// @clint:end mastra-index', '');
57
+ return lines.join('\n');
58
+ }
59
+ // Mastra enabled but no full agent config yet — placeholder
18
60
  return [
61
+ '// @clint:begin mastra-index',
19
62
  '/**',
20
63
  ' * Mastra Studio entry point for `mastra dev` / `mastra build` / `mastra start`.',
21
64
  ' *',
22
65
  " * Re-export a configured `@mastra/core` Mastra instance from here once you",
23
66
  " * replace the demo createAgent shim with a real Mastra Agent.",
24
67
  ' */',
25
- '// @clint:begin mastra-index',
26
68
  'export {};',
27
69
  '// @clint:end mastra-index',
28
70
  '',
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Builds the root `publish.config.ts` for split-layout projects.
3
+ * Defines a single lockstep group containing both the app and cli packages.
4
+ */
5
+ import { type ClintProjectSpec } from '../../spec/types.js';
6
+ export declare function buildPublishConfigTs(spec: ClintProjectSpec): string;
@@ -0,0 +1,21 @@
1
+ export function buildPublishConfigTs(spec) {
2
+ const app = `${spec.name}-app`;
3
+ const cli = `${spec.name}-cli`;
4
+ // The publish pipeline appends the prerelease suffix (e.g. -dev.0), so we
5
+ // only store the base semver (M.m.p) here.
6
+ const baseVersion = spec.version.replace(/-.*$/, '');
7
+ return `// Publish config — loaded by ph-clint's publish pipeline.
8
+ // definePublishConfig is an identity function; a plain export is equivalent.
9
+ export default {
10
+ groups: {
11
+ '${spec.name}': {
12
+ version: '${baseVersion}',
13
+ packages: [
14
+ { path: '${app}', category: 'app' },
15
+ { path: '${cli}', category: 'cli' },
16
+ ],
17
+ },
18
+ },
19
+ };
20
+ `;
21
+ }
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Builds the project README for a freshly-generated ph-clint project.
3
3
  */
4
- import { getPackageName, } from '../../spec/types.js';
4
+ import { getPackageName, phAtLeast, } from '../../spec/types.js';
5
5
  export function buildReadme(spec) {
6
6
  const { mastra, routine, powerhouse } = spec.features;
7
+ const ph = powerhouse;
7
8
  const title = getPackageName(spec);
8
9
  const lines = [];
9
10
  lines.push(`# ${title}`);
@@ -19,10 +20,10 @@ export function buildReadme(spec) {
19
20
  lines.push('');
20
21
  lines.push('## Enabled features');
21
22
  lines.push('');
22
- lines.push(`- **Powerhouse**: ${powerhouse.enabled ? 'on' : 'off'}`);
23
- if (powerhouse.enabled) {
24
- lines.push(` - Switchboard: ${powerhouse.switchboard ? 'on' : 'off'}`);
25
- lines.push(` - Connect: ${powerhouse.connect ? 'on' : 'off'}`);
23
+ lines.push(`- **Powerhouse**: ${ph !== 'Disabled' ? `on (${ph})` : 'off'}`);
24
+ if (phAtLeast(ph, 'Reactor')) {
25
+ lines.push(` - Switchboard: ${phAtLeast(ph, 'Switchboard') ? 'on' : 'off'}`);
26
+ lines.push(` - Connect: ${phAtLeast(ph, 'Connect') ? 'on' : 'off'}`);
26
27
  }
27
28
  lines.push(`- **Mastra agent**: ${mastra.enabled ? 'on' : 'off'}`);
28
29
  lines.push(`- **Routine loop**: ${routine.enabled ? 'on' : 'off'}`);
@@ -30,9 +31,9 @@ export function buildReadme(spec) {
30
31
  lines.push('## Regenerate');
31
32
  lines.push('');
32
33
  lines.push('Toggle features or update metadata in `.ph/ph-clint-cli/project-spec.json`,');
33
- lines.push('then re-run `ph-clint` to regenerate.');
34
+ lines.push('then re-run `ph-clint clint-project-regen` to regenerate.');
34
35
  lines.push('');
35
- if (powerhouse.enabled) {
36
+ if (phAtLeast(ph, 'Reactor')) {
36
37
  lines.push('## Split layout');
37
38
  lines.push('');
38
39
  lines.push(`This project is split into \`${spec.name}-cli/\` (the CLI) and \`${spec.name}-app/\` (the Powerhouse reactor package).`);
@@ -2,6 +2,9 @@
2
2
  * Builds the *root* `package.json` for split-layout projects (Powerhouse
3
3
  * enabled). A plain passthrough — deliberately no pnpm workspaces because
4
4
  * they interfere with `{name}-app`'s own `pnpm build`.
5
+ *
6
+ * Uses `pnpm --prefix <dir>` for all orchestration scripts — no cwd mutation.
7
+ * Build order: app first, then cli (cli depends on app via `file:` dep).
5
8
  */
6
9
  import { type ClintProjectSpec } from '../../spec/types.js';
7
10
  export declare function buildRootPackageJson(spec: ClintProjectSpec): string;
@@ -2,26 +2,39 @@
2
2
  * Builds the *root* `package.json` for split-layout projects (Powerhouse
3
3
  * enabled). A plain passthrough — deliberately no pnpm workspaces because
4
4
  * they interfere with `{name}-app`'s own `pnpm build`.
5
+ *
6
+ * Uses `pnpm --prefix <dir>` for all orchestration scripts — no cwd mutation.
7
+ * Build order: app first, then cli (cli depends on app via `file:` dep).
5
8
  */
6
- import { getPackageName, } from '../../spec/types.js';
9
+ import { getPackageName, phAtLeast, } from '../../spec/types.js';
7
10
  export function buildRootPackageJson(spec) {
8
11
  const cli = `${spec.name}-cli`;
9
12
  const app = `${spec.name}-app`;
13
+ const prefix = (dir, cmd) => `pnpm --prefix ${dir} ${cmd}`;
14
+ const both = (cmd) => `${prefix(app, cmd)} && ${prefix(cli, cmd)}`;
15
+ const connectEnabled = phAtLeast(spec.features.powerhouse, 'Connect');
16
+ const buildScript = connectEnabled
17
+ ? `${prefix(app, 'build')} && ${prefix(app, 'connect:build')} && ${prefix(cli, 'build')}`
18
+ : both('build');
19
+ const scripts = {
20
+ install: both('install'),
21
+ build: buildScript,
22
+ test: both('test'),
23
+ dev: prefix(cli, 'dev'),
24
+ start: prefix(cli, 'start'),
25
+ lint: prefix(cli, 'lint'),
26
+ 'app:dev': prefix(app, 'dev'),
27
+ 'cli:dev': prefix(cli, 'dev'),
28
+ 'publish:dev': `${prefix(cli, 'exec ph-publish')} dev -c ./publish.config.ts`,
29
+ 'publish:staging': `${prefix(cli, 'exec ph-publish')} staging -c ./publish.config.ts`,
30
+ 'publish:production': `${prefix(cli, 'exec ph-publish')} production -c ./publish.config.ts`,
31
+ };
10
32
  const pkg = {
11
33
  name: getPackageName(spec),
12
34
  version: spec.version,
13
35
  private: true,
14
36
  ...(spec.description ? { description: spec.description } : {}),
15
- scripts: {
16
- install: `cd ${app} && pnpm install && cd ../${cli} && pnpm install`,
17
- build: `cd ${app} && pnpm build && cd ../${cli} && pnpm build`,
18
- dev: `cd ${cli} && pnpm dev`,
19
- start: `cd ${cli} && pnpm start`,
20
- test: `cd ${app} && pnpm test && cd ../${cli} && pnpm test`,
21
- lint: `cd ${cli} && pnpm lint`,
22
- 'app:dev': `cd ${app} && pnpm dev`,
23
- 'cli:dev': `cd ${cli} && pnpm dev`,
24
- },
37
+ scripts,
25
38
  };
26
39
  return JSON.stringify(pkg, null, 2) + '\n';
27
40
  }
@@ -35,7 +35,7 @@ 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, buildPublishConfigTs, buildAppIndexTs, } from './builders/index.js';
39
39
  /**
40
40
  * Generate / reconcile a project at `targetDir` from the given spec.
41
41
  */
@@ -50,8 +50,8 @@ export async function generateProject(options) {
50
50
  let migrated = false;
51
51
  if (resolvedMode === 'update' &&
52
52
  existingSpec &&
53
- !existingSpec.features.powerhouse.enabled &&
54
- spec.features.powerhouse.enabled) {
53
+ existingSpec.features.powerhouse === 'Disabled' &&
54
+ spec.features.powerhouse !== 'Disabled') {
55
55
  await migrateFlatToSplit({
56
56
  targetDir,
57
57
  spec,
@@ -75,11 +75,12 @@ function resolveMode(requested, specExists) {
75
75
  }
76
76
  /** Build the list of files the new spec would emit, with their content. */
77
77
  function planFiles(spec, targetDir) {
78
- const split = spec.features.powerhouse.enabled;
78
+ const split = spec.features.powerhouse !== 'Disabled';
79
79
  const cliDir = split ? path.join(targetDir, getCliFolderName(spec)) : targetDir;
80
80
  const appDir = split ? path.join(targetDir, getAppFolderName(spec)) : null;
81
81
  const planned = [];
82
- for (const builder of CLI_FILE_BUILDERS) {
82
+ const allBuilders = [...CLI_FILE_BUILDERS, ...getProfileFileBuilders(spec)];
83
+ for (const builder of allBuilders) {
83
84
  const content = builder.build(spec);
84
85
  if (content === null)
85
86
  continue;
@@ -106,6 +107,12 @@ function planFiles(spec, targetDir) {
106
107
  absolutePath: rootPkg,
107
108
  content: buildRootPackageJson(spec),
108
109
  });
110
+ const publishConfig = path.join(targetDir, 'publish.config.ts');
111
+ planned.push({
112
+ relativePath: 'publish.config.ts',
113
+ absolutePath: publishConfig,
114
+ content: buildPublishConfigTs(spec),
115
+ });
109
116
  const appGitkeep = path.join(appDir, '.gitkeep');
110
117
  planned.push({
111
118
  relativePath: path.relative(targetDir, appGitkeep),
@@ -195,13 +202,20 @@ async function runUpdate(options, warn, migrated) {
195
202
  for (const p of planned) {
196
203
  if (p.initOnly) {
197
204
  // 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;
205
+ // are emitted only on create. In update mode we never overwrite them —
206
+ // but we DO create them if they don't yet exist on disk (e.g. first
207
+ // spec-change trigger run where no explicit `init` was run before).
208
+ const onDisk = await hashFile(p.absolutePath);
209
+ if (onDisk === null && p.content) {
210
+ await writeFileEnsuringDir(p.absolutePath, p.content);
211
+ next[p.relativePath] = hashContent(p.content);
212
+ files.push({ absolutePath: p.absolutePath, relativePath: p.relativePath });
213
+ }
214
+ else {
215
+ const stored = previous[p.relativePath];
216
+ if (stored !== undefined)
217
+ next[p.relativePath] = stored;
218
+ }
205
219
  continue;
206
220
  }
207
221
  const existing = await hashFile(p.absolutePath);
@@ -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,14 +17,22 @@ 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>;
21
37
  export interface PnpmInstallOptions {
22
38
  /** Directories to run `pnpm install` in, in order. */
@@ -24,7 +40,8 @@ export interface PnpmInstallOptions {
24
40
  log?: (msg: string) => void;
25
41
  /** Override `pnpm` binary name (for tests). */
26
42
  binName?: string;
27
- stdio?: 'inherit' | 'ignore';
43
+ /** Subprocess runner — uses CommandContext.runProcess when available. */
44
+ runProcess?: RunProcess;
28
45
  }
29
46
  export interface PnpmInstallResult {
30
47
  ran: string[];
@@ -32,3 +49,4 @@ export interface PnpmInstallResult {
32
49
  reason?: string;
33
50
  }
34
51
  export declare function runPnpmInstall(options: PnpmInstallOptions): Promise<PnpmInstallResult>;
52
+ export {};