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

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 +387 -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 +62 -8
  9. package/dist/codegen/builders/cli-ts.js +61 -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 +23 -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 -8
  41. package/dist/commands/clint-project-init.js +45 -41
  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
package/dist/cli.js CHANGED
@@ -7,9 +7,9 @@
7
7
  * Everything outside the markers is user-editable and preserved.
8
8
  */
9
9
  import path from 'node:path';
10
- import { fileURLToPath } from 'node:url';
11
10
  import { defineCli, buildDefaultReactor } from '@powerhousedao/ph-clint';
12
- import { CLI_NAME, CLI_VERSION } from './config.js';
11
+ import { z } from 'zod';
12
+ import { CLI_NAME, CLI_VERSION, CLI_ROOT } from './config.js';
13
13
  import { configSchema, secretsSchema } from './framework.js';
14
14
  // @clint:begin imports
15
15
  import { documentModels } from '@powerhousedao/ph-clint-app';
@@ -19,31 +19,68 @@ import { regen } from './commands/clint-project-regen.js';
19
19
  import { build } from './commands/clint-project-build.js';
20
20
  import { clintProjectPublish } from './commands/clint-project-publish.js';
21
21
  import { clintProject } from './services/clint-project.js';
22
+ import { skillsSync } from './commands/clint-skills-sync.js';
22
23
  import { specChangeTrigger } from './triggers/spec-change.js';
24
+ import { publishTrigger } from './triggers/publish-trigger.js';
23
25
  // @clint:end imports
24
- // Connect (ph connect) must run inside the Reactor Package (ph-clint-app).
25
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
- const appDir = path.resolve(__dirname, '../../ph-clint-app');
27
26
  export const cli = defineCli({
28
27
  name: CLI_NAME,
29
28
  version: CLI_VERSION,
29
+ root: CLI_ROOT,
30
30
  description: `${CLI_NAME} v${CLI_VERSION} — scaffold and maintain ph-clint implementation projects`,
31
31
  configSchema,
32
32
  secretsSchema,
33
33
  // @clint:begin commands
34
- commands: [init, regen, build, clintProjectPublish],
34
+ commands: [init, regen, build, clintProjectPublish, skillsSync],
35
35
  // @clint:end commands
36
36
  // @clint:begin services
37
37
  services: [clintProject],
38
38
  // @clint:end services
39
39
  // @clint:begin triggers
40
- triggers: [specChangeTrigger],
40
+ triggers: [specChangeTrigger, publishTrigger],
41
41
  // @clint:end triggers
42
42
  // @clint:begin prompts
43
43
  prompts: {
44
- sources: [],
45
- agents: {},
46
- skills: {},
44
+ artifacts: [path.join(CLI_ROOT, 'gen', 'skills'), path.join(CLI_ROOT, 'dist', 'gen', 'skills')],
45
+ agents: {
46
+ 'ph-clint-dev-agent': {
47
+ name: 'PhClintDevAgent',
48
+ sections: ['AgentBase.md'],
49
+ skills: ['cli-setup', 'command-definition', 'service-definition'],
50
+ },
51
+ },
52
+ skills: {
53
+ 'cli-setup': {
54
+ description: 'Set up a new CLI with defineCli, config schemas, entrypoint, and interactive mode',
55
+ inputSchema: z.object({
56
+ mode: z
57
+ .enum(['expert', 'discovery', 'one-shot'])
58
+ .default('expert')
59
+ .describe('Expert: align design decisions with the developer. Discovery: explain concepts step by step. One-shot: make all decisions autonomously'),
60
+ }),
61
+ instructionTemplate: 'Use your {{skillId}} skill in {{mode}} mode for: {{prompt}}',
62
+ },
63
+ 'command-definition': {
64
+ description: 'Define commands with Zod schemas, execute functions, return values, and parameter prompting',
65
+ inputSchema: z.object({
66
+ mode: z
67
+ .enum(['expert', 'discovery', 'one-shot'])
68
+ .default('expert')
69
+ .describe('Expert: align design decisions with the developer. Discovery: explain concepts step by step. One-shot: make all decisions autonomously'),
70
+ }),
71
+ instructionTemplate: 'Use your {{skillId}} skill in {{mode}} mode for: {{prompt}}',
72
+ },
73
+ 'service-definition': {
74
+ description: 'Define background services with readiness detection, preflight checks, and project scanning',
75
+ inputSchema: z.object({
76
+ mode: z
77
+ .enum(['expert', 'discovery', 'one-shot'])
78
+ .default('expert')
79
+ .describe('Expert: align design decisions with the developer. Discovery: explain concepts step by step. One-shot: make all decisions autonomously'),
80
+ }),
81
+ instructionTemplate: 'Use your {{skillId}} skill in {{mode}} mode for: {{prompt}}',
82
+ },
83
+ },
47
84
  },
48
85
  // @clint:end prompts
49
86
  // @clint:begin events
@@ -70,11 +107,13 @@ export const cli = defineCli({
70
107
  cli.configureReactor({
71
108
  create: (ctx) => buildDefaultReactor(ctx, {
72
109
  documentModels,
73
- drive: { name: 'Clint' },
110
+ drives: [
111
+ { name: 'Clint Folders', role: 'personal' },
112
+ ],
74
113
  subscriptions: { documentTypes: ['powerhouse/ph-clint-project'] },
75
114
  }),
76
115
  switchboard: { enabled: true },
77
- connect: { enabled: true, workdir: appDir },
116
+ connect: { enabled: true },
78
117
  });
79
118
  // @clint:end reactor
80
119
  // @clint:begin mastra
@@ -0,0 +1,42 @@
1
+ import type { GenerateProjectResult } from './index.js';
2
+ import type { ClintProjectSpec, ExternalSkill } from '../spec/types.js';
3
+ import type { ProcessRunOptions } from '@powerhousedao/ph-clint';
4
+ export type PostGenAction = {
5
+ kind: 'ph-init';
6
+ targetDir: string;
7
+ appDir: string;
8
+ spec: ClintProjectSpec;
9
+ } | {
10
+ kind: 'app-install';
11
+ dir: string;
12
+ } | {
13
+ kind: 'app-build';
14
+ dir: string;
15
+ } | {
16
+ kind: 'cli-install';
17
+ dir: string;
18
+ } | {
19
+ kind: 'cli-build';
20
+ dir: string;
21
+ } | {
22
+ kind: 'skills-sync';
23
+ targetDir: string;
24
+ desired: ExternalSkill[];
25
+ };
26
+ export type PostGenActionKind = PostGenAction['kind'];
27
+ export interface PostGenActionContext {
28
+ log: (msg: string) => void;
29
+ runProcess?: (command: string, opts?: Omit<ProcessRunOptions, 'onOutput'>) => Promise<{
30
+ success: boolean;
31
+ output: string;
32
+ }>;
33
+ skip?: Set<PostGenActionKind>;
34
+ }
35
+ export interface PostGenActionResult {
36
+ action: PostGenAction;
37
+ status: 'success' | 'failed' | 'skipped';
38
+ reason?: string;
39
+ durationMs: number;
40
+ }
41
+ export declare function collectPostGenActions(result: GenerateProjectResult, spec: ClintProjectSpec): Promise<PostGenAction[]>;
42
+ export declare function runPostGenActions(actions: PostGenAction[], ctx: PostGenActionContext): Promise<PostGenActionResult[]>;
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Post-generation actions — a typed pipeline that runs after codegen.
3
+ *
4
+ * `collectPostGenActions()` inspects a `GenerateProjectResult` to determine
5
+ * which side-effects (install, build, ph-init, skills-sync) are needed.
6
+ *
7
+ * `runPostGenActions()` executes them sequentially with proper ordering,
8
+ * failure cascading, and log output.
9
+ */
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ // ---------------------------------------------------------------------------
13
+ // Fixed execution order — index defines precedence
14
+ // ---------------------------------------------------------------------------
15
+ const ACTION_ORDER = [
16
+ 'ph-init',
17
+ 'app-install',
18
+ 'app-build',
19
+ 'cli-install',
20
+ 'cli-build',
21
+ 'skills-sync',
22
+ ];
23
+ function actionIndex(kind) {
24
+ return ACTION_ORDER.indexOf(kind);
25
+ }
26
+ // ---------------------------------------------------------------------------
27
+ // Dependency map — which actions depend on which (for failure cascading)
28
+ // ---------------------------------------------------------------------------
29
+ /** Returns true if `downstream` depends on `upstream` succeeding. */
30
+ function dependsOn(downstream, upstream) {
31
+ // ph-init → app-install → app-build → cli-install → cli-build
32
+ // skills-sync is independent of the build chain
33
+ const chain = [
34
+ 'ph-init',
35
+ 'app-install',
36
+ 'app-build',
37
+ 'cli-install',
38
+ 'cli-build',
39
+ ];
40
+ if (downstream === 'skills-sync')
41
+ return false;
42
+ const di = chain.indexOf(downstream);
43
+ const ui = chain.indexOf(upstream);
44
+ if (di < 0 || ui < 0)
45
+ return false;
46
+ return di > ui;
47
+ }
48
+ // ---------------------------------------------------------------------------
49
+ // Helpers
50
+ // ---------------------------------------------------------------------------
51
+ async function fileExists(filePath) {
52
+ try {
53
+ await fs.access(filePath);
54
+ return true;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // collectPostGenActions
62
+ // ---------------------------------------------------------------------------
63
+ export async function collectPostGenActions(result, spec) {
64
+ const split = result.appDir !== null;
65
+ const relPaths = new Set(result.files.map((f) => f.relativePath));
66
+ // Determine the earliest triggered action in the chain
67
+ let earliest = null;
68
+ if (split) {
69
+ const appFolder = result.appDir ? path.basename(result.appDir) : '';
70
+ const cliFolder = path.basename(result.cliDir);
71
+ // ph-init needed: app dir placeholder written but app not yet initialized.
72
+ // Check the actual filesystem — if package.json already exists on disk,
73
+ // ph init has already run and must not run again.
74
+ const appPkgJsonChanged = relPaths.has(path.join(appFolder, 'package.json'));
75
+ const appGitkeepWritten = relPaths.has(path.join(appFolder, '.gitkeep'));
76
+ // ph-init needed: app dir placeholder written but app not yet initialized.
77
+ // Check the actual filesystem — if package.json already exists on disk,
78
+ // ph init has already run and must not run again.
79
+ if (appGitkeepWritten && !appPkgJsonChanged) {
80
+ const appAlreadyInitialized = result.appDir
81
+ ? await fileExists(path.join(result.appDir, 'package.json'))
82
+ : false;
83
+ if (!appAlreadyInitialized) {
84
+ earliest = 'ph-init';
85
+ }
86
+ }
87
+ // Migration always triggers full rebuild
88
+ if (result.migrated) {
89
+ earliest = earliest ?? 'app-install';
90
+ }
91
+ // App package.json changed
92
+ if (!earliest && appPkgJsonChanged) {
93
+ earliest = 'app-install';
94
+ }
95
+ // App source regenerated (framework.gen.ts, index.ts, etc.)
96
+ if (!earliest) {
97
+ for (const rel of relPaths) {
98
+ if (rel.startsWith(appFolder + '/') &&
99
+ rel.endsWith('.ts')) {
100
+ earliest = 'app-build';
101
+ break;
102
+ }
103
+ }
104
+ }
105
+ // CLI package.json changed
106
+ if (!earliest) {
107
+ if (relPaths.has(path.join(cliFolder, 'package.json'))) {
108
+ earliest = 'cli-install';
109
+ }
110
+ }
111
+ // CLI source or prompts regenerated (.ts files, agent profile .md files)
112
+ if (!earliest) {
113
+ for (const rel of relPaths) {
114
+ if (rel.startsWith(cliFolder + '/') &&
115
+ (rel.endsWith('.ts') || rel.endsWith('.md'))) {
116
+ earliest = 'cli-build';
117
+ break;
118
+ }
119
+ }
120
+ }
121
+ }
122
+ else {
123
+ // Flat layout
124
+ if (relPaths.has('package.json')) {
125
+ earliest = 'cli-install';
126
+ }
127
+ else {
128
+ for (const rel of relPaths) {
129
+ if (rel.endsWith('.ts') || rel.endsWith('.md')) {
130
+ earliest = 'cli-build';
131
+ break;
132
+ }
133
+ }
134
+ }
135
+ }
136
+ // Build the action list from earliest onward
137
+ const actions = [];
138
+ if (earliest && split && result.appDir) {
139
+ const ei = actionIndex(earliest);
140
+ if (ei <= actionIndex('ph-init')) {
141
+ actions.push({
142
+ kind: 'ph-init',
143
+ targetDir: path.dirname(result.appDir),
144
+ appDir: result.appDir,
145
+ spec,
146
+ });
147
+ }
148
+ if (ei <= actionIndex('app-install')) {
149
+ actions.push({ kind: 'app-install', dir: result.appDir });
150
+ }
151
+ if (ei <= actionIndex('app-build')) {
152
+ // Add install if not already in the list and node_modules is missing
153
+ if (!actions.some(a => a.kind === 'app-install') &&
154
+ !(await fileExists(path.join(result.appDir, 'node_modules')))) {
155
+ actions.push({ kind: 'app-install', dir: result.appDir });
156
+ }
157
+ actions.push({ kind: 'app-build', dir: result.appDir });
158
+ }
159
+ if (ei <= actionIndex('cli-install')) {
160
+ actions.push({ kind: 'cli-install', dir: result.cliDir });
161
+ }
162
+ if (ei <= actionIndex('cli-build')) {
163
+ // Add install if not already in the list and node_modules is missing
164
+ if (!actions.some(a => a.kind === 'cli-install') &&
165
+ !(await fileExists(path.join(result.cliDir, 'node_modules')))) {
166
+ actions.push({ kind: 'cli-install', dir: result.cliDir });
167
+ }
168
+ actions.push({ kind: 'cli-build', dir: result.cliDir });
169
+ }
170
+ }
171
+ else if (earliest && !split) {
172
+ const ei = actionIndex(earliest);
173
+ if (ei <= actionIndex('cli-install')) {
174
+ actions.push({ kind: 'cli-install', dir: result.cliDir });
175
+ }
176
+ if (ei <= actionIndex('cli-build')) {
177
+ // Add install if not already in the list and node_modules is missing
178
+ if (!actions.some(a => a.kind === 'cli-install') &&
179
+ !(await fileExists(path.join(result.cliDir, 'node_modules')))) {
180
+ actions.push({ kind: 'cli-install', dir: result.cliDir });
181
+ }
182
+ actions.push({ kind: 'cli-build', dir: result.cliDir });
183
+ }
184
+ }
185
+ // skills-sync is independent — append if needed
186
+ if (spec.externalSkills.length > 0) {
187
+ const targetDir = split ? result.cliDir : result.cliDir;
188
+ actions.push({
189
+ kind: 'skills-sync',
190
+ targetDir,
191
+ desired: spec.externalSkills,
192
+ });
193
+ }
194
+ return actions;
195
+ }
196
+ // ---------------------------------------------------------------------------
197
+ // Human-readable label for an action
198
+ // ---------------------------------------------------------------------------
199
+ function actionLabel(action) {
200
+ switch (action.kind) {
201
+ case 'ph-init':
202
+ return `Initialize reactor package (${path.basename(action.appDir)})`;
203
+ case 'app-install':
204
+ return `Install dependencies (${path.basename(action.dir)})`;
205
+ case 'app-build':
206
+ return `Build (${path.basename(action.dir)})`;
207
+ case 'cli-install':
208
+ return `Install dependencies (${path.basename(action.dir)})`;
209
+ case 'cli-build':
210
+ return `Build (${path.basename(action.dir)})`;
211
+ case 'skills-sync':
212
+ return `Sync external skills (${action.desired.length} skill${action.desired.length === 1 ? '' : 's'})`;
213
+ }
214
+ }
215
+ // ---------------------------------------------------------------------------
216
+ // runPostGenActions — effectful runner
217
+ // ---------------------------------------------------------------------------
218
+ export async function runPostGenActions(actions, ctx) {
219
+ if (actions.length === 0)
220
+ return [];
221
+ const skip = ctx.skip ?? new Set();
222
+ // Filter out explicitly skipped actions. Also skip builds if their
223
+ // corresponding install is skipped (building without install is pointless).
224
+ const effective = actions.filter((a) => {
225
+ if (skip.has(a.kind))
226
+ return false;
227
+ // If app-install is skipped, also skip app-build
228
+ if (a.kind === 'app-build' && skip.has('app-install'))
229
+ return false;
230
+ // If cli-install is skipped, also skip cli-build
231
+ if (a.kind === 'cli-build' && skip.has('cli-install'))
232
+ return false;
233
+ return true;
234
+ });
235
+ if (effective.length === 0)
236
+ return [];
237
+ // Print numbered summary
238
+ ctx.log(`\nPost-generation steps (${effective.length}):`);
239
+ effective.forEach((a, i) => {
240
+ ctx.log(` ${i + 1}. ${actionLabel(a)}`);
241
+ });
242
+ ctx.log('');
243
+ const results = [];
244
+ const failed = new Set();
245
+ for (let i = 0; i < effective.length; i++) {
246
+ const action = effective[i];
247
+ const label = actionLabel(action);
248
+ const prefix = `[${i + 1}/${effective.length}]`;
249
+ // Check if a dependency failed
250
+ const failedDep = [...failed].find((f) => dependsOn(action.kind, f));
251
+ if (failedDep) {
252
+ ctx.log(`${prefix} ${label}... skipped (dependency failed)`);
253
+ results.push({
254
+ action,
255
+ status: 'skipped',
256
+ reason: 'dependency failed',
257
+ durationMs: 0,
258
+ });
259
+ continue;
260
+ }
261
+ ctx.log(`${prefix} ${label}...`);
262
+ const t0 = Date.now();
263
+ try {
264
+ const success = await executeAction(action, ctx);
265
+ const elapsed = Date.now() - t0;
266
+ if (success) {
267
+ ctx.log(` done (${formatDuration(elapsed)})`);
268
+ results.push({ action, status: 'success', durationMs: elapsed });
269
+ }
270
+ else {
271
+ ctx.log(` FAILED`);
272
+ results.push({ action, status: 'failed', durationMs: elapsed });
273
+ failed.add(action.kind);
274
+ }
275
+ }
276
+ catch (err) {
277
+ const elapsed = Date.now() - t0;
278
+ const msg = err instanceof Error ? err.message : String(err);
279
+ ctx.log(` FAILED: ${msg}`);
280
+ results.push({
281
+ action,
282
+ status: 'failed',
283
+ reason: msg,
284
+ durationMs: elapsed,
285
+ });
286
+ failed.add(action.kind);
287
+ }
288
+ }
289
+ // Print summary
290
+ const succeeded = results.filter((r) => r.status === 'success').length;
291
+ const failedCount = results.filter((r) => r.status === 'failed').length;
292
+ const skippedCount = results.filter((r) => r.status === 'skipped').length;
293
+ ctx.log('');
294
+ if (failedCount === 0 && skippedCount === 0) {
295
+ ctx.log(`Post-generation: ${succeeded} step${succeeded === 1 ? '' : 's'} completed successfully.`);
296
+ ctx.log('Ready — run `pnpm dev` to start.');
297
+ }
298
+ else {
299
+ ctx.log(`Post-generation: ${succeeded} succeeded, ${failedCount} failed, ${skippedCount} skipped.`);
300
+ if (failedCount > 0) {
301
+ ctx.log('Fix the reported error(s) and re-run `ph-clint clint-project-regen`.');
302
+ }
303
+ }
304
+ return results;
305
+ }
306
+ // ---------------------------------------------------------------------------
307
+ // Action dispatch
308
+ // ---------------------------------------------------------------------------
309
+ async function executeAction(action, ctx) {
310
+ switch (action.kind) {
311
+ case 'ph-init': {
312
+ const { runPhInit } = await import('./scaffold.js');
313
+ const result = await runPhInit({
314
+ targetDir: action.targetDir,
315
+ appDir: action.appDir,
316
+ spec: action.spec,
317
+ log: (msg) => ctx.log(` ${msg}`),
318
+ runProcess: ctx.runProcess,
319
+ });
320
+ if (!result.ran) {
321
+ ctx.log(` ${result.reason ?? 'ph init did not run'}`);
322
+ return false;
323
+ }
324
+ if (result.exitCode === 0) {
325
+ // Mark app as initialized in generated state
326
+ const { readGeneratedState, writeGeneratedState } = await import('./generated.js');
327
+ const gen = await readGeneratedState(action.targetDir);
328
+ if (gen) {
329
+ gen.appInitialized = true;
330
+ await writeGeneratedState(action.targetDir, gen);
331
+ }
332
+ }
333
+ return result.exitCode === 0;
334
+ }
335
+ case 'app-install':
336
+ case 'cli-install': {
337
+ if (ctx.runProcess) {
338
+ const result = await ctx.runProcess('pnpm install', {
339
+ cwd: action.dir,
340
+ timeout: 300_000,
341
+ });
342
+ return result.success;
343
+ }
344
+ // Fallback: direct spawn
345
+ const { runCommand } = await import('./exec.js');
346
+ const result = await runCommand('pnpm', ['install'], {
347
+ cwd: action.dir,
348
+ stdio: 'inherit',
349
+ });
350
+ return result.exitCode === 0;
351
+ }
352
+ case 'app-build':
353
+ case 'cli-build': {
354
+ return runBuild(action.dir, ctx);
355
+ }
356
+ case 'skills-sync': {
357
+ const { syncExternalSkills } = await import('../skills/sync.js');
358
+ await syncExternalSkills({
359
+ targetDir: action.targetDir,
360
+ desired: action.desired,
361
+ log: (msg) => ctx.log(` ${msg}`),
362
+ });
363
+ return true;
364
+ }
365
+ }
366
+ }
367
+ async function runBuild(dir, ctx) {
368
+ if (ctx.runProcess) {
369
+ const result = await ctx.runProcess('pnpm build', {
370
+ cwd: dir,
371
+ timeout: 120_000,
372
+ });
373
+ return result.success;
374
+ }
375
+ // Fallback: direct spawn
376
+ const { runCommand } = await import('./exec.js');
377
+ const result = await runCommand('pnpm', ['build'], {
378
+ cwd: dir,
379
+ stdio: 'inherit',
380
+ });
381
+ return result.exitCode === 0;
382
+ }
383
+ function formatDuration(ms) {
384
+ if (ms < 1000)
385
+ return `${ms}ms`;
386
+ return `${(ms / 1000).toFixed(1)}s`;
387
+ }
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Builds `src/agents/agent.ts` — a minimal Mastra agent factory.
2
+ * Builds `src/agents/agent.ts` — a Mastra agent factory.
3
3
  *
4
- * Emitted only when `features.mastra.enabled`. The generated agent falls back
5
- * to a deterministic echo-style demo provider when no API key is configured,
6
- * so `pnpm dev` works immediately out of the box.
4
+ * Emitted only when `features.mastra.enabled`. When the spec includes agent
5
+ * config (agentId, models, profiles), generates a real `@mastra/core` Agent.
6
+ * Otherwise falls back to a deterministic echo-style demo provider.
7
7
  */
8
8
  import { type ClintProjectSpec } from '../../spec/types.js';
9
9
  export declare function buildAgentTs(spec: ClintProjectSpec): string;
@@ -1,5 +1,18 @@
1
- export function buildAgentTs(spec) {
2
- const id = spec.name;
1
+ /** Map provider prefix to AI SDK package. */
2
+ function providerImport(provider) {
3
+ switch (provider) {
4
+ case 'anthropic':
5
+ return { pkg: '@ai-sdk/anthropic', fn: 'anthropic' };
6
+ case 'openai':
7
+ return { pkg: '@ai-sdk/openai', fn: 'openai' };
8
+ case 'google':
9
+ return { pkg: '@ai-sdk/google', fn: 'google' };
10
+ default:
11
+ return { pkg: `@ai-sdk/${provider}`, fn: provider };
12
+ }
13
+ }
14
+ function buildDemoAgent(spec) {
15
+ const id = spec.features.mastra.agentId ?? spec.name;
3
16
  return [
4
17
  "import type { AgentProvider, AgentSetupContext, StreamChunk } from '@powerhousedao/ph-clint';",
5
18
  "import type { Config } from '../framework.js';",
@@ -32,3 +45,71 @@ export function buildAgentTs(spec) {
32
45
  '',
33
46
  ].join('\n');
34
47
  }
48
+ function buildRealAgent(spec) {
49
+ const mastra = spec.features.mastra;
50
+ const agentId = mastra.agentId;
51
+ const agentName = mastra.agentName;
52
+ const defaultModel = mastra.models.find(m => m.isDefault) ?? mastra.models[0];
53
+ const modelId = defaultModel?.id ?? 'anthropic/claude-sonnet-4-5';
54
+ const [provider] = modelId.split(/[:/]/);
55
+ const { pkg, fn } = providerImport(provider);
56
+ const apiKeyField = `${provider}ApiKey`;
57
+ const lines = [];
58
+ lines.push("import { Agent } from '@mastra/core/agent';");
59
+ lines.push("import { MCPClient } from '@mastra/mcp';");
60
+ lines.push("import { createMastraHelpers } from '@powerhousedao/ph-clint/mastra';");
61
+ lines.push("import { createWorkdirStore } from '@powerhousedao/ph-clint';");
62
+ lines.push("import type { AgentSetupContext, AgentProvider } from '@powerhousedao/ph-clint';");
63
+ lines.push("import type { WrapAgentOptions } from '@powerhousedao/ph-clint/mastra';");
64
+ lines.push("import { CLI_NAME } from '../config.js';");
65
+ lines.push("import type { Config } from '../framework.js';");
66
+ lines.push("import { createDemoAgent } from './demo-agent.js';");
67
+ lines.push('');
68
+ lines.push('/**');
69
+ lines.push(' * Agent factory for the CLI.');
70
+ lines.push(' *');
71
+ lines.push(' * Returns a demo agent when no API key is configured, or wraps the full');
72
+ lines.push(' * agent as a ph-clint AgentProvider.');
73
+ lines.push(' */');
74
+ lines.push('export async function createAgent(ctx: AgentSetupContext<Config>): Promise<AgentProvider> {');
75
+ lines.push(` if (!ctx.config.${apiKeyField}) return createDemoAgent();`);
76
+ lines.push('');
77
+ lines.push(' const m = createMastraHelpers(ctx);');
78
+ lines.push('');
79
+ lines.push(' const agent = new Agent({');
80
+ lines.push(` id: '${agentId}',`);
81
+ lines.push(` name: '${agentName}',`);
82
+ lines.push(` instructions: m.getAgentInstructions('${agentId}'),`);
83
+ lines.push(` model: ctx.config.${apiKeyField}`);
84
+ lines.push(` ? { id: ctx.config.model as \`\${string}/\${string}\`, apiKey: ctx.config.${apiKeyField} }`);
85
+ lines.push(` : (ctx.config.model as \`\${string}/\${string}\`),`);
86
+ lines.push(' tools: async () => {');
87
+ lines.push(" ctx.context.log?.debug('[agent] tools callback invoked');");
88
+ lines.push(' const tools = await m.getTools({ MCPClient });');
89
+ lines.push(" ctx.context.log?.debug(`[agent] tools resolved: ${Object.keys(tools).length} tools`);");
90
+ lines.push(' return tools;');
91
+ lines.push(' },');
92
+ lines.push(' workspace: await m.createWorkspace(),');
93
+ lines.push(' memory: await m.createMemory(),');
94
+ lines.push(' });');
95
+ lines.push('');
96
+ lines.push(' const store = createWorkdirStore(ctx.workdir, CLI_NAME);');
97
+ lines.push(' const wrapOpts: WrapAgentOptions = {');
98
+ lines.push(' maxSteps: 80,');
99
+ lines.push(' enableLogging: ctx.config.agentLogging,');
100
+ lines.push(" logDirectory: store.getStoreFolder('logs'),");
101
+ lines.push(' cacheControl: true,');
102
+ lines.push(' };');
103
+ lines.push(' return m.wrapAgent(agent, wrapOpts);');
104
+ lines.push('}');
105
+ lines.push('');
106
+ return lines.join('\n');
107
+ }
108
+ export function buildAgentTs(spec) {
109
+ const mastra = spec.features.mastra;
110
+ // Generate real agent when we have full config, demo otherwise
111
+ if (mastra.agentId && mastra.models.length > 0) {
112
+ return buildRealAgent(spec);
113
+ }
114
+ return buildDemoAgent(spec);
115
+ }
@@ -3,16 +3,9 @@
3
3
  * reactor package. Impl code imports from `'{name}-app'` instead of drilling
4
4
  * into `'{name}-app/document-models/<slug>/v1/gen/...'`.
5
5
  *
6
- * Each entry re-exports a document-model subfolder's `index.ts`, which in
7
- * turn re-exports the module object (`PhClintProject`, ...), action creators,
8
- * document/state types, and type guards. Fully machine-owned: regen on every
9
- * run.
10
- *
11
- * Powerhouse's own codegen might emit its own `index.ts` (as is the case for
12
- * ph-clint-app today, which has manifest/editors/processors re-exports). When
13
- * that exists, leave it alone — users hand-merge. Our codegen only emits when
14
- * the file is absent or was emitted by a previous ph-clint-cli run (hash
15
- * matches the ledger).
6
+ * Only re-exports document types belonging to the app package (the project's
7
+ * own reactor package). External packages' document types are imported from
8
+ * their own npm packages, not from the app barrel.
16
9
  */
17
10
  import { type ClintProjectSpec } from '../../spec/types.js';
18
11
  export declare function buildAppIndexTs(spec: ClintProjectSpec): string | null;