@powerhousedao/ph-clint-cli 0.1.0-dev.50 → 0.1.0-dev.52

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.
@@ -30,7 +30,7 @@ export type PostGenAction = {
30
30
  export type PostGenActionKind = PostGenAction['kind'];
31
31
  export interface PostGenActionContext {
32
32
  log: (msg: string) => void;
33
- runProcess?: (command: string, opts?: Omit<ProcessRunOptions, 'onOutput'>) => Promise<{
33
+ runProcess: (command: string, opts?: Omit<ProcessRunOptions, 'onOutput'>) => Promise<{
34
34
  success: boolean;
35
35
  output: string;
36
36
  }>;
@@ -389,20 +389,11 @@ async function executeAction(action, ctx) {
389
389
  }
390
390
  case 'app-install':
391
391
  case 'cli-install': {
392
- if (ctx.runProcess) {
393
- const result = await ctx.runProcess('pnpm install', {
394
- cwd: action.dir,
395
- timeout: 300_000,
396
- });
397
- return result.success;
398
- }
399
- // Fallback: direct spawn
400
- const { runCommand } = await import('./exec.js');
401
- const result = await runCommand('pnpm', ['install'], {
392
+ const result = await ctx.runProcess('pnpm install', {
402
393
  cwd: action.dir,
403
- stdio: 'inherit',
394
+ timeout: 300_000,
404
395
  });
405
- return result.exitCode === 0;
396
+ return result.success;
406
397
  }
407
398
  case 'app-build':
408
399
  case 'cli-build': {
@@ -420,20 +411,11 @@ async function executeAction(action, ctx) {
420
411
  }
421
412
  }
422
413
  async function runBuild(dir, ctx) {
423
- if (ctx.runProcess) {
424
- const result = await ctx.runProcess('pnpm build', {
425
- cwd: dir,
426
- timeout: 120_000,
427
- });
428
- return result.success;
429
- }
430
- // Fallback: direct spawn
431
- const { runCommand } = await import('./exec.js');
432
- const result = await runCommand('pnpm', ['build'], {
414
+ const result = await ctx.runProcess('pnpm build', {
433
415
  cwd: dir,
434
- stdio: 'inherit',
416
+ timeout: 120_000,
435
417
  });
436
- return result.exitCode === 0;
418
+ return result.success;
437
419
  }
438
420
  function formatDuration(ms) {
439
421
  if (ms < 1000)
@@ -7,7 +7,7 @@
7
7
  * own reactor package). External packages' document types are imported from
8
8
  * their own npm packages, not from the app barrel.
9
9
  */
10
- import { getAppPackageName, getDocumentTypeSlug, phAtLeast, } from '../../spec/types.js';
10
+ import { getAppPackageName, getDocumentTypeSlug, isDocTypeGlob, phAtLeast, } from '../../spec/types.js';
11
11
  export function buildAppIndexTs(spec) {
12
12
  if (!phAtLeast(spec.features.powerhouse, 'Reactor'))
13
13
  return null;
@@ -16,12 +16,22 @@ export function buildAppIndexTs(spec) {
16
16
  const docTypes = appEntry?.documentTypes ?? [];
17
17
  if (docTypes.length === 0)
18
18
  return null;
19
+ const hasGlobs = docTypes.some(isDocTypeGlob);
20
+ const explicits = docTypes.filter((dt) => !isDocTypeGlob(dt));
19
21
  const lines = [];
20
22
  lines.push('// THIS FILE IS GENERATED BY ph-clint-cli.');
21
23
  lines.push('// Re-exports every registered document model from a single top-level');
22
24
  lines.push('// entry so impl code can `import { ... } from \'{name}-app\'`.');
23
25
  lines.push('');
24
- for (const docType of docTypes) {
26
+ if (hasGlobs) {
27
+ // Glob patterns can't enumerate slugs at codegen time — re-export
28
+ // from the Powerhouse-generated barrel which already re-exports all
29
+ // document models in the package.
30
+ lines.push("export * from './document-models/index.js';");
31
+ }
32
+ // Explicit entries still get per-slug re-exports (they may point at
33
+ // models not yet in the generated barrel during bootstrapping).
34
+ for (const docType of explicits) {
25
35
  const slug = getDocumentTypeSlug(docType);
26
36
  lines.push(`export * from './document-models/${slug}/index.js';`);
27
37
  }
@@ -1,13 +1,19 @@
1
1
  /**
2
2
  * Builds `src/framework.gen.ts` — the fully machine-owned half of the
3
3
  * typed-binding split. Regenerated on every `init` and `update`; holds the
4
- * `defineRegistry([...] as const)` call that describes which document models
4
+ * `defineRegistry([...])` call that describes which document models
5
5
  * are bound into `framework.ts`'s typed factories.
6
6
  *
7
7
  * Iterates all packages' document types. The app package's modules are
8
8
  * imported from the local `{name}-app` package; external packages' modules
9
9
  * are imported from their npm package name.
10
10
  *
11
+ * Document type entries may be explicit IDs (`powerhouse/ph-clint-project`)
12
+ * or glob patterns (`*​/*`, `powerhouse/*`, `acme/doc-*`). Explicit IDs
13
+ * produce static named imports; glob patterns import the package's
14
+ * `documentModels` array and filter at runtime so newly-added document
15
+ * models are picked up without a codegen re-run.
16
+ *
11
17
  * Emitted only when Powerhouse is enabled — flat-layout impls have no
12
18
  * reactor package to import modules from and bind `createTypes` directly in
13
19
  * `framework.ts`. When Powerhouse is on but no document types are declared,
@@ -1,13 +1,19 @@
1
1
  /**
2
2
  * Builds `src/framework.gen.ts` — the fully machine-owned half of the
3
3
  * typed-binding split. Regenerated on every `init` and `update`; holds the
4
- * `defineRegistry([...] as const)` call that describes which document models
4
+ * `defineRegistry([...])` call that describes which document models
5
5
  * are bound into `framework.ts`'s typed factories.
6
6
  *
7
7
  * Iterates all packages' document types. The app package's modules are
8
8
  * imported from the local `{name}-app` package; external packages' modules
9
9
  * are imported from their npm package name.
10
10
  *
11
+ * Document type entries may be explicit IDs (`powerhouse/ph-clint-project`)
12
+ * or glob patterns (`*​/*`, `powerhouse/*`, `acme/doc-*`). Explicit IDs
13
+ * produce static named imports; glob patterns import the package's
14
+ * `documentModels` array and filter at runtime so newly-added document
15
+ * models are picked up without a codegen re-run.
16
+ *
11
17
  * Emitted only when Powerhouse is enabled — flat-layout impls have no
12
18
  * reactor package to import modules from and bind `createTypes` directly in
13
19
  * `framework.ts`. When Powerhouse is on but no document types are declared,
@@ -15,28 +21,44 @@
15
21
  * `import { registry } from './framework.gen.js'` keeps resolving across
16
22
  * regens.
17
23
  */
18
- import { getAppPackageName, getDocumentTypeModuleName, getDocumentTypeSlug, phAtLeast, } from '../../spec/types.js';
24
+ import { docTypeGlobToRegex, getAppPackageName, getDocumentTypeModuleName, getDocumentTypeSlug, isDocTypeGlob, phAtLeast, } from '../../spec/types.js';
19
25
  export function buildFrameworkGenTs(spec) {
20
26
  if (!phAtLeast(spec.features.powerhouse, 'Reactor'))
21
27
  return null;
22
28
  const appPkg = getAppPackageName(spec);
23
- // Collect all (docType, moduleName, importFrom) tuples across packages.
24
- // App-package modules are imported from the app barrel which re-exports
25
- // bare PascalCase names. External packages use the per-slug deep subpath
26
- // (`<pkg>/document-models/<slug>`) so TypeScript can resolve internal
27
- // types (e.g. PHState) from stable import paths for declaration emit.
29
+ // Collect entries across packages. Each package may contribute both
30
+ // explicit named imports and glob-based runtime filters.
28
31
  const entries = [];
32
+ let globCounter = 0;
29
33
  for (const pkg of spec.packages) {
30
34
  const isApp = pkg.packageName === appPkg;
31
- for (const docType of pkg.documentTypes) {
35
+ // Collect glob patterns for this package (may be multiple).
36
+ const globs = pkg.documentTypes.filter(isDocTypeGlob);
37
+ const explicits = pkg.documentTypes.filter((dt) => !isDocTypeGlob(dt));
38
+ for (const docType of explicits) {
32
39
  const slug = getDocumentTypeSlug(docType);
33
40
  entries.push({
34
- docType,
41
+ kind: 'explicit',
35
42
  name: getDocumentTypeModuleName(docType),
36
43
  importFrom: isApp ? appPkg : `${pkg.packageName}/document-models/${slug}`,
37
44
  });
38
45
  }
46
+ if (globs.length > 0) {
47
+ // Merge all globs for this package into a single regex alternation.
48
+ const combined = globs.length === 1
49
+ ? docTypeGlobToRegex(globs[0])
50
+ : globs.map((g) => `(?:${docTypeGlobToRegex(g)})`).join('|');
51
+ const alias = globCounter === 0 ? 'globModels' : `globModels${globCounter}`;
52
+ globCounter++;
53
+ entries.push({
54
+ kind: 'glob',
55
+ modelsAlias: alias,
56
+ importFrom: isApp ? appPkg : pkg.packageName,
57
+ regexSource: combined,
58
+ });
59
+ }
39
60
  }
61
+ const hasGlobs = entries.some((e) => e.kind === 'glob');
40
62
  const lines = [];
41
63
  lines.push('/**');
42
64
  lines.push(' * THIS FILE IS GENERATED BY ph-clint-cli. DO NOT EDIT.');
@@ -46,8 +68,14 @@ export function buildFrameworkGenTs(spec) {
46
68
  lines.push(' * this file will be overwritten.');
47
69
  lines.push(' */');
48
70
  lines.push(`import { defineRegistry } from '@powerhousedao/ph-clint';`);
71
+ // Emit imports — named imports for explicit entries, documentModels for globs.
49
72
  for (const e of entries) {
50
- lines.push(`import { ${e.name} } from '${e.importFrom}';`);
73
+ if (e.kind === 'explicit') {
74
+ lines.push(`import { ${e.name} } from '${e.importFrom}';`);
75
+ }
76
+ else {
77
+ lines.push(`import { documentModels as ${e.modelsAlias} } from '${e.importFrom}';`);
78
+ }
51
79
  }
52
80
  lines.push('');
53
81
  lines.push('/**');
@@ -57,13 +85,29 @@ export function buildFrameworkGenTs(spec) {
57
85
  if (entries.length === 0) {
58
86
  lines.push('export const registry = defineRegistry([] as const);');
59
87
  }
60
- else {
88
+ else if (!hasGlobs) {
89
+ // Pure explicit — use `as const` for full type inference.
61
90
  lines.push('export const registry = defineRegistry([');
62
91
  for (const e of entries) {
63
- lines.push(` ${e.name},`);
92
+ if (e.kind === 'explicit') {
93
+ lines.push(` ${e.name},`);
94
+ }
64
95
  }
65
96
  lines.push('] as const);');
66
97
  }
98
+ else {
99
+ // Mix of explicit + glob — runtime filter loses `as const` inference.
100
+ lines.push('export const registry = defineRegistry([');
101
+ for (const e of entries) {
102
+ if (e.kind === 'explicit') {
103
+ lines.push(` ${e.name},`);
104
+ }
105
+ else {
106
+ lines.push(` ...${e.modelsAlias}.filter((m) => /${e.regexSource}/.test(m.documentModel.global.id)),`);
107
+ }
108
+ }
109
+ lines.push(']);');
110
+ }
67
111
  lines.push('');
68
112
  lines.push('export type Registry = typeof registry;');
69
113
  return lines.join('\n') + '\n';
@@ -49,4 +49,3 @@ export { buildCliPackageJson, buildCliTs, buildConfigTs, buildMainTs, buildTscon
49
49
  export { buildRootPackageJson } from './root-package-json.js';
50
50
  export { buildPublishConfigJs } from './publish-config-js.js';
51
51
  export { buildReadme } from './readme-md.js';
52
- export { buildAppIndexTs } from './app-index-ts.js';
@@ -82,4 +82,3 @@ export { buildCliPackageJson, buildCliTs, buildConfigTs, buildMainTs, buildTscon
82
82
  export { buildRootPackageJson } from './root-package-json.js';
83
83
  export { buildPublishConfigJs } from './publish-config-js.js';
84
84
  export { buildReadme } from './readme-md.js';
85
- export { buildAppIndexTs } from './app-index-ts.js';
@@ -1,20 +1,2 @@
1
- export interface RunCommandOptions {
2
- cwd: string;
3
- /** Override stdio; default `'inherit'`. Pass `'ignore'` for tests. */
4
- stdio?: 'inherit' | 'ignore';
5
- /** Extra env vars (merged with process.env). */
6
- env?: Record<string, string>;
7
- }
8
- export interface RunCommandResult {
9
- command: string;
10
- args: string[];
11
- cwd: string;
12
- exitCode: number;
13
- }
14
- /**
15
- * Run `command args` in `cwd`, forwarding stdio by default. Resolves to
16
- * the exit code. Does NOT throw on non-zero — callers decide how to react.
17
- */
18
- export declare function runCommand(command: string, args: string[], options: RunCommandOptions): Promise<RunCommandResult>;
19
1
  /** Returns true if `command` is on PATH. */
20
2
  export declare function hasCommandOnPath(command: string): Promise<boolean>;
@@ -1,34 +1,7 @@
1
1
  /**
2
2
  * Small helpers for invoking external commands during code generation.
3
- *
4
- * We deliberately keep this separate from `ph-clint`'s process manager:
5
- * here we need synchronous-feeling prompt-forwarding (stdio inherited) so
6
- * the user can respond to interactive prompts from `ph init` or from a
7
- * package manager.
8
3
  */
9
4
  import { spawn } from 'node:child_process';
10
- /**
11
- * Run `command args` in `cwd`, forwarding stdio by default. Resolves to
12
- * the exit code. Does NOT throw on non-zero — callers decide how to react.
13
- */
14
- export async function runCommand(command, args, options) {
15
- return new Promise((resolve, reject) => {
16
- const child = spawn(command, args, {
17
- cwd: options.cwd,
18
- stdio: options.stdio ?? 'inherit',
19
- env: options.env ? { ...process.env, ...options.env } : process.env,
20
- });
21
- child.once('error', reject);
22
- child.once('close', (code) => {
23
- resolve({
24
- command,
25
- args,
26
- cwd: options.cwd,
27
- exitCode: code ?? 0,
28
- });
29
- });
30
- });
31
- }
32
5
  /** Returns true if `command` is on PATH. */
33
6
  export async function hasCommandOnPath(command) {
34
7
  return new Promise((resolve) => {
@@ -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, getProfileFileBuilders, buildReadme, buildRootPackageJson, buildPublishConfigJs, buildAppIndexTs, } from './builders/index.js';
38
+ import { CLI_FILE_BUILDERS, getProfileFileBuilders, buildReadme, buildRootPackageJson, buildPublishConfigJs, } from './builders/index.js';
39
39
  import { collectPostGenActions, } from './actions.js';
40
40
  import { readGeneratedState, writeGeneratedState, generatedStateFromSpec, } from './generated.js';
41
41
  import { patchAppPackageName } from './scaffold.js';
@@ -135,17 +135,6 @@ function planFiles(spec, targetDir) {
135
135
  absolutePath: appReadme,
136
136
  content: buildAppReadmeContent(spec),
137
137
  });
138
- // Top-level barrel for the reactor package — lets impl code import from
139
- // `{name}-app` instead of drilling into `document-models/<slug>/v1/gen`.
140
- const appIndex = buildAppIndexTs(spec);
141
- if (appIndex !== null) {
142
- const appIndexAbs = path.join(appDir, 'index.ts');
143
- planned.push({
144
- relativePath: path.relative(targetDir, appIndexAbs),
145
- absolutePath: appIndexAbs,
146
- content: appIndex,
147
- });
148
- }
149
138
  }
150
139
  return { planned, cliDir, appDir };
151
140
  }
@@ -7,6 +7,7 @@ type RunProcess = (command: string, opts?: Omit<ProcessRunOptions, 'onOutput'>)
7
7
  success: boolean;
8
8
  output: string;
9
9
  }>;
10
+ export type { RunProcess };
10
11
  export interface PhInitOptions {
11
12
  /** Absolute path to the project root. */
12
13
  targetDir: string;
@@ -17,8 +18,8 @@ export interface PhInitOptions {
17
18
  log?: (msg: string) => void;
18
19
  /** Override `ph` binary name (for tests). */
19
20
  binName?: string;
20
- /** Subprocess runner — uses CommandContext.runProcess when available. */
21
- runProcess?: RunProcess;
21
+ /** Subprocess runner — matches CommandContext.runProcess. */
22
+ runProcess: RunProcess;
22
23
  /** Explicit Powerhouse version to pin `ph init` to (tag or semver). */
23
24
  phVersion?: string;
24
25
  }
@@ -52,7 +53,7 @@ export declare function runPhInstallPackages(options: {
52
53
  appDir: string;
53
54
  packages: string[];
54
55
  log?: (msg: string) => void;
55
- runProcess?: RunProcess;
56
+ runProcess: RunProcess;
56
57
  }): Promise<boolean>;
57
58
  export interface PnpmInstallOptions {
58
59
  /** Directories to run `pnpm install` in, in order. */
@@ -60,8 +61,8 @@ export interface PnpmInstallOptions {
60
61
  log?: (msg: string) => void;
61
62
  /** Override `pnpm` binary name (for tests). */
62
63
  binName?: string;
63
- /** Subprocess runner — uses CommandContext.runProcess when available. */
64
- runProcess?: RunProcess;
64
+ /** Subprocess runner — matches CommandContext.runProcess. */
65
+ runProcess: RunProcess;
65
66
  }
66
67
  export interface PnpmInstallResult {
67
68
  ran: string[];
@@ -69,4 +70,3 @@ export interface PnpmInstallResult {
69
70
  reason?: string;
70
71
  }
71
72
  export declare function runPnpmInstall(options: PnpmInstallOptions): Promise<PnpmInstallResult>;
72
- export {};
@@ -60,30 +60,17 @@ export async function runPhInit(options) {
60
60
  await fs.rm(options.appDir, { recursive: true, force: true });
61
61
  const args = ['init', appFolder, ...versionArgs, '--pnpm'];
62
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' });
81
- if (result.exitCode !== 0) {
82
- log(`\`${binName} init\` exited with code ${result.exitCode}. ` +
63
+ const result = await options.runProcess(`${binName} ${args.join(' ')}`, {
64
+ cwd: options.targetDir,
65
+ timeout: 300_000,
66
+ });
67
+ const exitCode = result.success ? 0 : 1;
68
+ if (!result.success) {
69
+ log(`\`${binName} init\` failed. ` +
83
70
  'You can re-run it manually after resolving the issue.');
84
71
  }
85
72
  await patchScopedName(options, appFolder, log);
86
- return { ran: true, exitCode: result.exitCode };
73
+ return { ran: true, exitCode };
87
74
  }
88
75
  /**
89
76
  * Patch the app `package.json` name to the scoped form (`@scope/name-app`).
@@ -132,19 +119,11 @@ export async function runPhInstallPackages(options) {
132
119
  }
133
120
  const args = ['install', ...packages, '--local', '--pnpm'];
134
121
  log(`Running \`${binName} install ${packages.join(' ')} --local\` in ${path.basename(appDir)} …`);
135
- if (runProcess) {
136
- const result = await runProcess(`${binName} ${args.join(' ')}`, {
137
- cwd: appDir,
138
- timeout: 300_000,
139
- });
140
- return result.success;
141
- }
142
- const { runCommand } = await import('./exec.js');
143
- const result = await runCommand(binName, args, {
122
+ const result = await runProcess(`${binName} ${args.join(' ')}`, {
144
123
  cwd: appDir,
145
- stdio: 'inherit',
124
+ timeout: 300_000,
146
125
  });
147
- return result.exitCode === 0;
126
+ return result.success;
148
127
  }
149
128
  export async function runPnpmInstall(options) {
150
129
  const log = options.log ?? (() => { });
@@ -157,27 +136,13 @@ export async function runPnpmInstall(options) {
157
136
  const ran = [];
158
137
  for (const dir of options.dirs) {
159
138
  log(`Running \`${binName} install\` in ${dir} …`);
160
- const run = options.runProcess;
161
- if (run) {
162
- const result = await run(`${binName} install`, {
163
- cwd: dir,
164
- timeout: 300_000,
165
- });
166
- if (!result.success) {
167
- log(`\`${binName} install\` failed in ${dir}. ` +
168
- 'Retry manually once the underlying issue is resolved.');
169
- }
170
- }
171
- else {
172
- const { runCommand } = await import('./exec.js');
173
- const result = await runCommand(binName, ['install'], {
174
- cwd: dir,
175
- stdio: 'inherit',
176
- });
177
- if (result.exitCode !== 0) {
178
- log(`\`${binName} install\` exited with code ${result.exitCode} in ${dir}. ` +
179
- 'Retry manually once the underlying issue is resolved.');
180
- }
139
+ const result = await options.runProcess(`${binName} install`, {
140
+ cwd: dir,
141
+ timeout: 300_000,
142
+ });
143
+ if (!result.success) {
144
+ log(`\`${binName} install\` failed in ${dir}. ` +
145
+ 'Retry manually once the underlying issue is resolved.');
181
146
  }
182
147
  ran.push(dir);
183
148
  }
@@ -4,4 +4,9 @@ export declare const build: import("@powerhousedao/ph-clint").Command<z.ZodObjec
4
4
  verbose: z.ZodDefault<z.ZodBoolean>;
5
5
  }, z.core.$strip>, {
6
6
  text: string;
7
- }, Record<string, unknown>>;
7
+ }, {
8
+ model: string;
9
+ devServicePort: number;
10
+ phVersion: string;
11
+ apiKey?: string | undefined;
12
+ }, import("@powerhousedao/ph-clint").InferRegistry<readonly [import("document-model").DocumentModelModule<import("@powerhousedao/ph-clint-app").PhClintProjectPHState>]>>;
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { defineCommand } from '@powerhousedao/ph-clint';
2
+ import { defineCommand } from '../framework.js';
3
3
  import { z } from 'zod';
4
4
  import { detectLayout } from '@powerhousedao/ph-clint-dev';
5
5
  const inputSchema = z.object({
@@ -78,5 +78,10 @@ export declare const init: import("@powerhousedao/ph-clint").Command<z.ZodObject
78
78
  };
79
79
  fileCount: number;
80
80
  };
81
- }, Record<string, unknown>>;
81
+ }, {
82
+ model: string;
83
+ devServicePort: number;
84
+ phVersion: string;
85
+ apiKey?: string | undefined;
86
+ }, import("@powerhousedao/ph-clint").InferRegistry<readonly [import("document-model").DocumentModelModule<import("@powerhousedao/ph-clint-app").PhClintProjectPHState>]>>;
82
87
  export {};
@@ -12,7 +12,7 @@
12
12
  * 8. Print next-steps guidance.
13
13
  */
14
14
  import path from 'node:path';
15
- import { defineCommand } from '@powerhousedao/ph-clint';
15
+ import { defineCommand } from '../framework.js';
16
16
  import { z } from 'zod';
17
17
  import { clintProjectSpecSchema, } from '../spec/types.js';
18
18
  import { generateProject } from '../codegen/index.js';
@@ -49,29 +49,33 @@ const inputSchema = z.object({
49
49
  .default(false)
50
50
  .describe('Do not run `pnpm install` after scaffolding'),
51
51
  });
52
- /** Parse `@scope/name` into { scope, name }. */
52
+ /** Parse `@scope/name` into { scope, name }. Auto-appends `-cli` if missing. */
53
53
  function splitPackageName(raw) {
54
54
  const match = /^@([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/.exec(raw);
55
- if (match)
56
- return { scope: `@${match[1]}`, name: match[2] };
55
+ if (match) {
56
+ const name = match[2].endsWith('-cli') ? match[2] : `${match[2]}-cli`;
57
+ return { scope: `@${match[1]}`, name };
58
+ }
57
59
  if (raw.startsWith('@')) {
58
60
  throw new Error(`invalid package name "${raw}" — expected "@scope/name" with lowercase letters, digits, hyphens`);
59
61
  }
60
62
  if (!/^[a-z0-9][a-z0-9-]*$/.test(raw)) {
61
63
  throw new Error(`invalid project name "${raw}" — expected lowercase letters, digits, hyphens`);
62
64
  }
63
- return { name: raw };
65
+ const name = raw.endsWith('-cli') ? raw : `${raw}-cli`;
66
+ return { name };
64
67
  }
65
68
  export function buildSpec(input) {
66
69
  const { scope, name } = splitPackageName(input.name);
67
70
  // Routine is forced on when Mastra is on — the REPL-only path works, but
68
71
  // the agent routine loop is what most users actually want.
69
72
  const routine = input.enableRoutine || input.enableMastra;
73
+ const baseName = name.replace(/-cli$/, '');
70
74
  const mastra = input.enableMastra
71
75
  ? {
72
76
  enabled: true,
73
- agentId: `${name}-agent`,
74
- agentName: `${name.charAt(0).toUpperCase()}${name.slice(1).replace(/-(\w)/g, (_, c) => ' ' + c.toUpperCase())} Agent`,
77
+ agentId: `${baseName}-agent`,
78
+ agentName: `${baseName.charAt(0).toUpperCase()}${baseName.slice(1).replace(/-(\w)/g, (_, c) => ' ' + c.toUpperCase())} Agent`,
75
79
  models: [{ id: 'anthropic/claude-sonnet-4-5', isDefault: true }],
76
80
  profiles: [{ id: 'base', title: 'Base', content: 'You are a helpful assistant.' }],
77
81
  }
@@ -13,4 +13,9 @@ export declare const clintProjectPublish: import("@powerhousedao/ph-clint").Comm
13
13
  }, z.core.$strip>, {
14
14
  text: string;
15
15
  data: import("@powerhousedao/ph-clint-dev/publish").PublishResult;
16
- }, Record<string, unknown>>;
16
+ }, {
17
+ model: string;
18
+ devServicePort: number;
19
+ phVersion: string;
20
+ apiKey?: string | undefined;
21
+ }, import("@powerhousedao/ph-clint").InferRegistry<readonly [import("document-model").DocumentModelModule<import("@powerhousedao/ph-clint-app").PhClintProjectPHState>]>>;
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { defineCommand } from '@powerhousedao/ph-clint';
2
+ import { defineCommand } from '../framework.js';
3
3
  import { z } from 'zod';
4
4
  import { resolvePublishPlan, buildPackages, publishPackages, } from '@powerhousedao/ph-clint-dev/publish';
5
5
  const inputSchema = z.object({
@@ -15,4 +15,9 @@ export declare const regen: import("@powerhousedao/ph-clint").Command<z.ZodObjec
15
15
  migrated: boolean;
16
16
  warnings: string[];
17
17
  };
18
- }, Record<string, unknown>>;
18
+ }, {
19
+ model: string;
20
+ devServicePort: number;
21
+ phVersion: string;
22
+ apiKey?: string | undefined;
23
+ }, import("@powerhousedao/ph-clint").InferRegistry<readonly [import("document-model").DocumentModelModule<import("@powerhousedao/ph-clint-app").PhClintProjectPHState>]>>;
@@ -7,7 +7,7 @@
7
7
  * migration fires first.
8
8
  */
9
9
  import path from 'node:path';
10
- import { defineCommand } from '@powerhousedao/ph-clint';
10
+ import { defineCommand } from '../framework.js';
11
11
  import { z } from 'zod';
12
12
  import { readProjectSpec } from '../spec/file.js';
13
13
  import { generateProject } from '../codegen/index.js';
@@ -7,4 +7,9 @@ export declare const skillsSync: import("@powerhousedao/ph-clint").Command<z.Zod
7
7
  } | {
8
8
  text: string;
9
9
  data: import("../skills/sync.js").SyncResult;
10
- }, Record<string, unknown>>;
10
+ }, {
11
+ model: string;
12
+ devServicePort: number;
13
+ phVersion: string;
14
+ apiKey?: string | undefined;
15
+ }, import("@powerhousedao/ph-clint").InferRegistry<readonly [import("document-model").DocumentModelModule<import("@powerhousedao/ph-clint-app").PhClintProjectPHState>]>>;
@@ -4,7 +4,7 @@
4
4
  * longer referenced, and updates `.skills-manifest.json`.
5
5
  */
6
6
  import path from 'node:path';
7
- import { defineCommand } from '@powerhousedao/ph-clint';
7
+ import { defineCommand } from '../framework.js';
8
8
  import { z } from 'zod';
9
9
  import { readProjectSpec } from '../spec/file.js';
10
10
  import { syncExternalSkills } from '../skills/sync.js';
@@ -36,7 +36,7 @@ export declare const defineCommand: <TInput extends z.ZodType, TOutput = unknown
36
36
  devServicePort: number;
37
37
  phVersion: string;
38
38
  apiKey?: string | undefined;
39
- }>, defineTrigger: <TState = Record<string, unknown>>(options: import("@powerhousedao/ph-clint").TriggerOptions<TState, {
39
+ }, import("@powerhousedao/ph-clint").InferRegistry<readonly [import("document-model").DocumentModelModule<import("@powerhousedao/ph-clint-app").PhClintProjectPHState>]>>, defineTrigger: <TState = Record<string, unknown>>(options: import("@powerhousedao/ph-clint").TriggerOptions<TState, {
40
40
  model: string;
41
41
  devServicePort: number;
42
42
  phVersion: string;
@@ -143,3 +143,22 @@ export declare function getDocumentTypeSlug(documentType: string): string;
143
143
  * it needs both for the import and the `defineRegistry` entry.
144
144
  */
145
145
  export declare function getDocumentTypeModuleName(documentType: string): string;
146
+ /**
147
+ * Returns true when a documentType entry contains glob wildcards (`*`).
148
+ * Glob entries like `*​/*`, `powerhouse/*`, or `powerhouse/doc-*` are spec
149
+ * shorthand meaning "all matching document types from this package" and
150
+ * must be resolved at runtime rather than emitted as static imports.
151
+ */
152
+ export declare function isDocTypeGlob(documentType: string): boolean;
153
+ /**
154
+ * Convert a documentType glob pattern to a regex source string suitable
155
+ * for embedding in a `/pattern/` literal. Each `*` matches any non-`/`
156
+ * characters. Forward slashes are escaped so the output is safe inside
157
+ * `/.../`.
158
+ *
159
+ * Examples:
160
+ * `*​/*` → `^[^\/]*\/[^\/]*$` (any org, any name)
161
+ * `powerhouse/*` → `^powerhouse\/[^\/]*$`
162
+ * `powerhouse/doc-*` → `^powerhouse\/doc-[^\/]*$`
163
+ */
164
+ export declare function docTypeGlobToRegex(pattern: string): string;
@@ -182,3 +182,28 @@ export function getDocumentTypeModuleName(documentType) {
182
182
  .map((s) => s[0].toUpperCase() + s.slice(1))
183
183
  .join('');
184
184
  }
185
+ /**
186
+ * Returns true when a documentType entry contains glob wildcards (`*`).
187
+ * Glob entries like `*​/*`, `powerhouse/*`, or `powerhouse/doc-*` are spec
188
+ * shorthand meaning "all matching document types from this package" and
189
+ * must be resolved at runtime rather than emitted as static imports.
190
+ */
191
+ export function isDocTypeGlob(documentType) {
192
+ return documentType.includes('*');
193
+ }
194
+ /**
195
+ * Convert a documentType glob pattern to a regex source string suitable
196
+ * for embedding in a `/pattern/` literal. Each `*` matches any non-`/`
197
+ * characters. Forward slashes are escaped so the output is safe inside
198
+ * `/.../`.
199
+ *
200
+ * Examples:
201
+ * `*​/*` → `^[^\/]*\/[^\/]*$` (any org, any name)
202
+ * `powerhouse/*` → `^powerhouse\/[^\/]*$`
203
+ * `powerhouse/doc-*` → `^powerhouse\/doc-[^\/]*$`
204
+ */
205
+ export function docTypeGlobToRegex(pattern) {
206
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\\/]/g, '\\$&');
207
+ const withWildcards = escaped.replace(/\*/g, '[^\\/]*');
208
+ return `^${withWildcards}$`;
209
+ }
@@ -18,7 +18,6 @@
18
18
  * b. Runs `ph-clint clint-project-publish --tag <tag>`.
19
19
  * c. Dispatches SET_PUBLISH_STATUS({ id, status: "Succeeded"|"Failed" }).
20
20
  */
21
- import { spawn } from 'node:child_process';
22
21
  import path from 'node:path';
23
22
  import { scanProjects, getProjectMapping } from '@powerhousedao/ph-clint';
24
23
  import { setPublishStatus } from '@powerhousedao/ph-clint-app';
@@ -27,37 +26,6 @@ import { specFromDocumentState } from '../spec/from-document.js';
27
26
  import { clintProject } from '../services/clint-project.js';
28
27
  const DOCUMENT_TYPE = 'powerhouse/ph-clint-project';
29
28
  const TAG = '[publish]';
30
- /** Run a shell command and return stdout. Rejects on non-zero exit. */
31
- function exec(cmd, args, cwd, onLog) {
32
- return new Promise((resolve, reject) => {
33
- const proc = spawn(cmd, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
34
- const out = [];
35
- const err = [];
36
- proc.stdout?.on('data', (d) => {
37
- const s = d.toString();
38
- out.push(s);
39
- if (onLog)
40
- for (const line of s.split('\n'))
41
- if (line.trim())
42
- onLog(line.trim());
43
- });
44
- proc.stderr?.on('data', (d) => {
45
- const s = d.toString();
46
- err.push(s);
47
- if (onLog)
48
- for (const line of s.split('\n'))
49
- if (line.trim())
50
- onLog(line.trim());
51
- });
52
- proc.on('error', reject);
53
- proc.on('close', (code) => {
54
- if (code === 0)
55
- resolve(out.join(''));
56
- else
57
- reject(new Error(`${cmd} ${args.join(' ')} exited ${code}\n${err.join('')}`));
58
- });
59
- });
60
- }
61
29
  /** Map document model PublishTag values to CLI --tag values. */
62
30
  export function tagToCli(tag) {
63
31
  switch (tag) {
@@ -107,6 +75,7 @@ export const publishTrigger = createDocumentChangeTrigger({
107
75
  continue;
108
76
  }
109
77
  log?.info(`${TAG} found pending ${record.tag} publish for doc ${doc.header.id} (id=${record.id}, version=${record.version})`);
78
+ const { runProcess, stdout } = ctx.context;
110
79
  return {
111
80
  type: 'function',
112
81
  params: {
@@ -122,25 +91,20 @@ export const publishTrigger = createDocumentChangeTrigger({
122
91
  setPublishStatus({ id: record.id, status: 'InProgress' }),
123
92
  ]);
124
93
  // Install dependencies in the generated project before publishing.
125
- log?.info(`${TAG} running pnpm install in ${workdir}`);
126
- try {
127
- await exec('pnpm', ['install'], workdir, (msg) => log?.debug(`${TAG} [install] ${msg}`));
128
- }
129
- catch (installErr) {
130
- log?.error(`${TAG} pnpm install failed: ${installErr instanceof Error ? installErr.message : String(installErr)}`);
94
+ stdout(`${TAG} running pnpm install in ${workdir}\n`);
95
+ const installResult = await runProcess('pnpm install', {
96
+ cwd: workdir,
97
+ timeout: 300_000,
98
+ });
99
+ if (!installResult.success) {
100
+ log?.error(`${TAG} pnpm install failed in ${workdir}`);
131
101
  }
132
102
  // Run the publish command.
133
103
  let success = false;
134
104
  try {
135
- log?.info(`${TAG} invoking clint-project-publish --tag ${cliTag}`);
105
+ stdout(`${TAG} invoking clint-project-publish --tag ${cliTag}\n`);
136
106
  const { clintProjectPublish } = await import('../commands/clint-project-publish.js');
137
- const result = await clintProjectPublish.execute({ tag: cliTag, dryRun: false, skipBuild: false, skipGitCheck: true, verbose: false },
138
- // @ts-expect-error — minimal context; publish uses workdir + stdout.
139
- {
140
- workdir,
141
- stdout: (msg) => log?.info(`${TAG} ${msg}`),
142
- log,
143
- });
107
+ const result = await clintProjectPublish.execute({ tag: cliTag, dryRun: false, skipBuild: false, skipGitCheck: true, verbose: false }, ctx.commandContext);
144
108
  success = !result.text.includes('failed');
145
109
  }
146
110
  catch (err) {
@@ -148,7 +112,7 @@ export const publishTrigger = createDocumentChangeTrigger({
148
112
  }
149
113
  // Update status.
150
114
  const finalStatus = success ? 'Succeeded' : 'Failed';
151
- log?.info(`${TAG} marking record ${record.id} as ${finalStatus}`);
115
+ stdout(`${TAG} marking record ${record.id} as ${finalStatus}\n`);
152
116
  await reactor.client.execute(doc.header.id, 'main', [
153
117
  setPublishStatus({
154
118
  id: record.id,
@@ -187,8 +187,10 @@ export const specChangeTrigger = createDocumentChangeTrigger({
187
187
  log.warn(`${TAG} ${w}`);
188
188
  }
189
189
  // Run post-generation actions (ph-init, install, build, skills-sync).
190
+ // Route through ctx.context.stdout so output flows through
191
+ // routine.onOutput → service log buffer → ServicePanel.
190
192
  await runPostGenActions(result.pendingActions, {
191
- log: (msg) => log?.info(`${TAG} ${msg}`),
193
+ log: (msg) => ctx.context.stdout(`${TAG} ${msg}\n`),
192
194
  runProcess: ctx.context.runProcess,
193
195
  });
194
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powerhousedao/ph-clint-cli",
3
- "version": "0.1.0-dev.50",
3
+ "version": "0.1.0-dev.52",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -29,9 +29,9 @@
29
29
  "@mastra/core": "^1.22.0",
30
30
  "@mastra/libsql": "^1.7.4",
31
31
  "@mastra/memory": "^1.13.1",
32
- "@powerhousedao/ph-clint": "^0.1.0-dev.50",
33
- "@powerhousedao/ph-clint-app": "^0.1.0-dev.50",
34
- "@powerhousedao/ph-clint-dev": "^0.1.0-dev.50",
32
+ "@powerhousedao/ph-clint": "^0.1.0-dev.52",
33
+ "@powerhousedao/ph-clint-app": "^0.1.0-dev.52",
34
+ "@powerhousedao/ph-clint-dev": "^0.1.0-dev.52",
35
35
  "@powerhousedao/reactor": "6.0.0-dev.170",
36
36
  "@powerhousedao/reactor-api": "6.0.0-dev.170",
37
37
  "@powerhousedao/shared": "6.0.0-dev.170",