@powerhousedao/ph-clint-cli 0.1.0-dev.0 → 0.1.0-dev.10

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.
package/dist/cli.js CHANGED
@@ -7,20 +7,26 @@
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';
10
+ import { existsSync } from 'node:fs';
11
11
  import { defineCli, buildDefaultReactor } from '@powerhousedao/ph-clint';
12
- import { CLI_NAME, CLI_VERSION } from './config.js';
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';
16
16
  import { createAgent } from './agents/clint-agent.js';
17
- import { init } from './commands/init.js';
18
- import { regen } from './commands/regen.js';
17
+ import { init } from './commands/clint-project-init.js';
18
+ import { regen } from './commands/clint-project-regen.js';
19
+ import { build } from './commands/clint-project-build.js';
20
+ import { clintProjectPublish } from './commands/clint-project-publish.js';
21
+ import { clintProject } from './services/clint-project.js';
19
22
  import { specChangeTrigger } from './triggers/spec-change.js';
20
23
  // @clint:end imports
21
24
  // Connect (ph connect) must run inside the Reactor Package (ph-clint-app).
22
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
- const appDir = path.resolve(__dirname, '../../ph-clint-app');
25
+ const appDir = path.resolve(CLI_ROOT, '../ph-clint-app');
26
+ function resolveConnectAssets(dir) {
27
+ const assetsDir = path.join(dir, 'dist', 'connect');
28
+ return existsSync(path.join(assetsDir, 'index.html')) ? assetsDir : undefined;
29
+ }
24
30
  export const cli = defineCli({
25
31
  name: CLI_NAME,
26
32
  version: CLI_VERSION,
@@ -28,10 +34,10 @@ export const cli = defineCli({
28
34
  configSchema,
29
35
  secretsSchema,
30
36
  // @clint:begin commands
31
- commands: [init, regen],
37
+ commands: [init, regen, build, clintProjectPublish],
32
38
  // @clint:end commands
33
39
  // @clint:begin services
34
- services: [],
40
+ services: [clintProject],
35
41
  // @clint:end services
36
42
  // @clint:begin triggers
37
43
  triggers: [specChangeTrigger],
@@ -71,7 +77,7 @@ cli.configureReactor({
71
77
  subscriptions: { documentTypes: ['powerhouse/ph-clint-project'] },
72
78
  }),
73
79
  switchboard: { enabled: true },
74
- connect: { enabled: true, workdir: appDir },
80
+ connect: { enabled: true, workdir: appDir, assetsDir: resolveConnectAssets(appDir) },
75
81
  });
76
82
  // @clint:end reactor
77
83
  // @clint:begin mastra
@@ -5,8 +5,36 @@
5
5
  * `{root}/{name}-cli/package.json`. Otherwise at `{root}/package.json`.
6
6
  */
7
7
  import { getBinName, getPackageName, } from '../../spec/types.js';
8
- /** Pinned version range for peer ph-clint packages emitted into generated projects. */
9
- const PH_CLINT_VERSION = '^0.1.0';
8
+ import { CLI_VERSION } from '../../config.js';
9
+ /**
10
+ * Derive a semver range from the running ph-clint-cli version that respects
11
+ * the dev / staging / production channel boundary. This is the version range
12
+ * emitted into *scaffolded* projects' package.json for @powerhousedao/ph-clint
13
+ * and @powerhousedao/ph-clint-dev dependencies.
14
+ *
15
+ * CLI_VERSION is the version of ph-clint-cli itself (read from its own
16
+ * package.json at startup). Since all framework packages share a lockstep
17
+ * version, this is also the correct base for the dependency range.
18
+ *
19
+ * Examples:
20
+ * "0.1.0" → "^0.1.0" (production — stable only)
21
+ * "0.1.0-staging.2" → "^0.1.0-staging.0" (staging + stable)
22
+ * "0.1.0-dev.4" → "^0.1.0-dev.0" (dev + staging + stable)
23
+ *
24
+ * This works because semver orders prereleases alphabetically:
25
+ * dev.N < staging.N < release. A caret range starting at dev.0 naturally
26
+ * upgrades through staging to production.
27
+ */
28
+ function phClintRange(version) {
29
+ const match = /^(\d+\.\d+\.\d+)(?:-([a-z]+)\.\d+)?/.exec(version);
30
+ if (!match)
31
+ return `^${version}`;
32
+ const base = match[1];
33
+ const channel = match[2]; // "dev", "staging", or undefined
34
+ return channel ? `^${base}-${channel}.0` : `^${base}`;
35
+ }
36
+ /** Version range for @powerhousedao/ph-clint{,-dev} deps in scaffolded projects. */
37
+ const PH_CLINT_VERSION = phClintRange(CLI_VERSION);
10
38
  const POWERHOUSE_VERSION = '6.0.0-dev.170';
11
39
  export function buildCliPackageJson(spec) {
12
40
  const { mastra, powerhouse } = spec.features;
@@ -15,7 +43,7 @@ export function buildCliPackageJson(spec) {
15
43
  : getPackageName(spec);
16
44
  const bin = getBinName(spec);
17
45
  const dependencies = {
18
- 'ph-clint': PH_CLINT_VERSION,
46
+ '@powerhousedao/ph-clint': PH_CLINT_VERSION,
19
47
  zod: '^4.3.6',
20
48
  };
21
49
  if (mastra.enabled) {
@@ -43,7 +71,6 @@ export function buildCliPackageJson(spec) {
43
71
  scripts['mastra:build'] = 'mastra build';
44
72
  scripts['mastra:start'] = 'mastra start';
45
73
  }
46
- scripts['publish:npm'] = 'pnpm build && pnpm publish --no-git-checks';
47
74
  const pkg = {
48
75
  name: pkgName,
49
76
  version: spec.version,
@@ -59,7 +86,7 @@ export function buildCliPackageJson(spec) {
59
86
  '@jest/globals': '^30.2.0',
60
87
  '@types/node': '^25.5.2',
61
88
  jest: '^30.2.0',
62
- 'ph-clint-dev': PH_CLINT_VERSION,
89
+ '@powerhousedao/ph-clint-dev': PH_CLINT_VERSION,
63
90
  'ts-jest': '^29.4.6',
64
91
  tsx: '^4.19.0',
65
92
  typescript: '^6.0.2',
@@ -10,13 +10,13 @@ export function buildCliTs(spec) {
10
10
  lines.push(' */');
11
11
  if (powerhouse.enabled) {
12
12
  lines.push(`import path from 'node:path';`);
13
- lines.push(`import { fileURLToPath } from 'node:url';`);
13
+ lines.push(`import { existsSync } from 'node:fs';`);
14
14
  lines.push(`import { defineCli, buildDefaultReactor } from '@powerhousedao/ph-clint';`);
15
15
  }
16
16
  else {
17
17
  lines.push(`import { defineCli } from '@powerhousedao/ph-clint';`);
18
18
  }
19
- lines.push(`import { CLI_NAME, CLI_VERSION } from './config.js';`);
19
+ lines.push(`import { CLI_NAME, CLI_VERSION${powerhouse.enabled ? ', CLI_ROOT' : ''} } from './config.js';`);
20
20
  lines.push(`import { configSchema, secretsSchema } from './framework.js';`);
21
21
  lines.push('');
22
22
  lines.push('// @clint:begin imports');
@@ -29,9 +29,15 @@ export function buildCliTs(spec) {
29
29
  lines.push('// @clint:end imports');
30
30
  lines.push('');
31
31
  if (powerhouse.enabled) {
32
- lines.push(`const __dirname = path.dirname(fileURLToPath(import.meta.url));`);
33
- lines.push(`const appDir = path.resolve(__dirname, '../../${spec.name}-app');`);
32
+ lines.push(`const appDir = path.resolve(CLI_ROOT, '../${spec.name}-app');`);
34
33
  lines.push('');
34
+ if (powerhouse.connect) {
35
+ lines.push('function resolveConnectAssets(dir: string): string | undefined {');
36
+ lines.push(" const assetsDir = path.join(dir, 'dist', 'connect');");
37
+ lines.push(" return existsSync(path.join(assetsDir, 'index.html')) ? assetsDir : undefined;");
38
+ lines.push('}');
39
+ lines.push('');
40
+ }
35
41
  }
36
42
  lines.push('export const cli = defineCli({');
37
43
  lines.push(' name: CLI_NAME,');
@@ -71,10 +77,21 @@ export function buildCliTs(spec) {
71
77
  lines.push('');
72
78
  lines.push(' // @clint:begin interactive');
73
79
  lines.push(' interactive: {');
74
- lines.push(` welcome: ({ workdir }) =>`);
80
+ const ctxArgs = [];
81
+ if (powerhouse.enabled && (powerhouse.switchboard || powerhouse.connect)) {
82
+ ctxArgs.push('config');
83
+ }
84
+ ctxArgs.push('workdir');
85
+ lines.push(` welcome: ({ ${ctxArgs.join(', ')} }) =>`);
75
86
  lines.push(' [');
76
87
  lines.push(' `${CLI_NAME} v${CLI_VERSION}`,');
77
88
  lines.push(' `Workdir: ${workdir}`,');
89
+ if (powerhouse.enabled && powerhouse.switchboard) {
90
+ lines.push(' `Switchboard: http://localhost:${config.switchboardPort}`,');
91
+ }
92
+ if (powerhouse.enabled && powerhouse.connect) {
93
+ lines.push(' `Connect: http://localhost:${config.connectPort}`,');
94
+ }
78
95
  lines.push(" '',");
79
96
  if (mastra.enabled) {
80
97
  lines.push(" 'Type a message to talk to the agent, or /help for commands.',");
@@ -96,7 +113,12 @@ export function buildCliTs(spec) {
96
113
  lines.push(` drive: { name: '${spec.name}' },`);
97
114
  lines.push(' }),');
98
115
  lines.push(` switchboard: { enabled: ${String(powerhouse.switchboard)} },`);
99
- lines.push(` connect: { enabled: ${String(powerhouse.connect)}, workdir: appDir },`);
116
+ if (powerhouse.connect) {
117
+ lines.push(` connect: { enabled: true, workdir: appDir, assetsDir: resolveConnectAssets(appDir) },`);
118
+ }
119
+ else {
120
+ lines.push(` connect: { enabled: false },`);
121
+ }
100
122
  lines.push('});');
101
123
  }
102
124
  lines.push('// @clint:end reactor');
@@ -1,21 +1,17 @@
1
- /**
2
- * Builds `src/config.ts` — CLI identity constants (name, version, project
3
- * root path). The Zod `configSchema` / `secretsSchema` are emitted by
4
- * `buildFrameworkTs` into `src/framework.ts` (user-owned, init-only) so users
5
- * can extend them without fighting the generator.
6
- */
7
- import { getBinName, } from '../../spec/types.js';
8
1
  export function buildConfigTs(spec) {
9
- const cliName = getBinName(spec);
2
+ const hasCustomBin = !!spec.bin;
10
3
  const lines = [];
11
- lines.push(`import path from 'node:path';`);
12
- lines.push(`import { fileURLToPath } from 'node:url';`);
4
+ lines.push(`import { readPackageInfo } from '@powerhousedao/ph-clint';`);
13
5
  lines.push('');
14
- lines.push('/** CLI name — used for config resolution, env var prefixing, and .ph/ paths. */');
15
- lines.push(`export const CLI_NAME = '${cliName}';`);
16
- lines.push(`export const CLI_VERSION = '${spec.version}';`);
6
+ lines.push('const pkg = readPackageInfo(import.meta.url);');
17
7
  lines.push('');
18
- lines.push('/** Project root — resolved from this file location. */');
19
- lines.push('export const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), \'..\');');
8
+ lines.push('export const CLI_ROOT = pkg.root;');
9
+ if (hasCustomBin) {
10
+ lines.push(`export const CLI_NAME = '${spec.bin}';`);
11
+ }
12
+ else {
13
+ lines.push(`export const CLI_NAME = pkg.name.replace(/-cli$/, '');`);
14
+ }
15
+ lines.push('export const CLI_VERSION = pkg.version;');
20
16
  return lines.join('\n') + '\n';
21
17
  }
@@ -33,7 +33,7 @@ export function buildFrameworkGenTs(spec) {
33
33
  lines.push('/**');
34
34
  lines.push(' * THIS FILE IS GENERATED BY ph-clint-cli. DO NOT EDIT.');
35
35
  lines.push(' *');
36
- lines.push(' * Regenerated on every `ph-clint regen` and on every spec-change');
36
+ lines.push(' * Regenerated on every `ph-clint clint-project-regen` and on every spec-change');
37
37
  lines.push(' * tick. Put custom bindings in `framework.ts`, not here — edits to');
38
38
  lines.push(' * this file will be overwritten.');
39
39
  lines.push(' */');
@@ -39,5 +39,6 @@ export interface FileBuilder {
39
39
  export declare const CLI_FILE_BUILDERS: FileBuilder[];
40
40
  export { buildCliPackageJson, buildCliTs, buildConfigTs, buildMainTs, buildTsconfigJson, buildJestConfigJs, buildEslintConfigJs, buildGitignore, buildMastraIndexTs, buildBuildSkillsScript, buildAgentBaseMd, buildAgentTs, buildFrameworkGenTs, buildFrameworkTs, };
41
41
  export { buildRootPackageJson } from './root-package-json.js';
42
+ export { buildPublishConfigTs } from './publish-config-ts.js';
42
43
  export { buildReadme } from './readme-md.js';
43
44
  export { buildAppIndexTs } from './app-index-ts.js';
@@ -56,5 +56,6 @@ export const CLI_FILE_BUILDERS = [
56
56
  ];
57
57
  export { buildCliPackageJson, buildCliTs, buildConfigTs, buildMainTs, buildTsconfigJson, buildJestConfigJs, buildEslintConfigJs, buildGitignore, buildMastraIndexTs, buildBuildSkillsScript, buildAgentBaseMd, buildAgentTs, buildFrameworkGenTs, buildFrameworkTs, };
58
58
  export { buildRootPackageJson } from './root-package-json.js';
59
+ export { buildPublishConfigTs } from './publish-config-ts.js';
59
60
  export { buildReadme } from './readme-md.js';
60
61
  export { buildAppIndexTs } from './app-index-ts.js';
@@ -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,18 @@
1
+ export function buildPublishConfigTs(spec) {
2
+ const app = `${spec.name}-app`;
3
+ const cli = `${spec.name}-cli`;
4
+ return `import { definePublishConfig } from '@powerhousedao/ph-clint-dev/publish';
5
+
6
+ export default definePublishConfig({
7
+ groups: {
8
+ '${spec.name}': {
9
+ version: '${spec.version}',
10
+ packages: [
11
+ { path: '${app}', category: 'app' },
12
+ { path: '${cli}', category: 'cli' },
13
+ ],
14
+ },
15
+ },
16
+ });
17
+ `;
18
+ }
@@ -30,7 +30,7 @@ export function buildReadme(spec) {
30
30
  lines.push('## Regenerate');
31
31
  lines.push('');
32
32
  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.');
33
+ lines.push('then re-run `ph-clint clint-project-regen` to regenerate.');
34
34
  lines.push('');
35
35
  if (powerhouse.enabled) {
36
36
  lines.push('## Split layout');
@@ -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
9
  import { getPackageName, } 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 = spec.features.powerhouse.enabled && 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, buildReadme, buildRootPackageJson, buildPublishConfigTs, buildAppIndexTs, } from './builders/index.js';
39
39
  /**
40
40
  * Generate / reconcile a project at `targetDir` from the given spec.
41
41
  */
@@ -106,6 +106,12 @@ function planFiles(spec, targetDir) {
106
106
  absolutePath: rootPkg,
107
107
  content: buildRootPackageJson(spec),
108
108
  });
109
+ const publishConfig = path.join(targetDir, 'publish.config.ts');
110
+ planned.push({
111
+ relativePath: 'publish.config.ts',
112
+ absolutePath: publishConfig,
113
+ content: buildPublishConfigTs(spec),
114
+ });
109
115
  const appGitkeep = path.join(appDir, '.gitkeep');
110
116
  planned.push({
111
117
  relativePath: path.relative(targetDir, appGitkeep),
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+ export declare const build: import("@powerhousedao/ph-clint").Command<z.ZodObject<{
3
+ dir: z.ZodOptional<z.ZodString>;
4
+ verbose: z.ZodDefault<z.ZodBoolean>;
5
+ }, z.core.$strip>, {
6
+ text: string;
7
+ }, Record<string, unknown>>;
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import { spawn } from 'node:child_process';
3
+ import { defineCommand } from '@powerhousedao/ph-clint';
4
+ import { z } from 'zod';
5
+ import { detectLayout } from '@powerhousedao/ph-clint-dev';
6
+ const inputSchema = z.object({
7
+ dir: z.string().optional().describe('Project root directory (default: cwd)'),
8
+ verbose: z.boolean().default(false),
9
+ });
10
+ function execInDir(dir, cmd, verbose) {
11
+ return new Promise((resolve, reject) => {
12
+ const proc = spawn(cmd, {
13
+ cwd: dir,
14
+ shell: true,
15
+ stdio: verbose ? 'inherit' : 'pipe',
16
+ });
17
+ let stderr = '';
18
+ if (!verbose && proc.stderr) {
19
+ proc.stderr.on('data', (data) => {
20
+ stderr += data.toString();
21
+ });
22
+ }
23
+ proc.on('close', (code) => {
24
+ if (code === 0)
25
+ resolve();
26
+ else
27
+ reject(new Error(`Command "${cmd}" failed in ${dir} (exit ${code})${stderr ? `:\n${stderr}` : ''}`));
28
+ });
29
+ proc.on('error', (err) => reject(err));
30
+ });
31
+ }
32
+ export const build = defineCommand({
33
+ id: 'clint-project-build',
34
+ description: 'Build all packages in the project',
35
+ inputSchema,
36
+ execute: async (input, { workdir, stdout }) => {
37
+ const dir = path.resolve(input.dir ?? workdir);
38
+ const layout = detectLayout(dir);
39
+ if (layout?.type === 'split') {
40
+ stdout(`Building app package (${layout.app})...\n`);
41
+ await execInDir(layout.app, 'pnpm build', input.verbose);
42
+ stdout(`Building cli package (${layout.cli})...\n`);
43
+ await execInDir(layout.cli, 'pnpm build', input.verbose);
44
+ }
45
+ else {
46
+ stdout(`Building project...\n`);
47
+ await execInDir(layout?.cli ?? dir, 'pnpm build', input.verbose);
48
+ }
49
+ return { text: 'Build complete.' };
50
+ },
51
+ });
@@ -0,0 +1,57 @@
1
+ import { z } from 'zod';
2
+ import { type ClintProjectSpec } from '../spec/types.js';
3
+ declare const inputSchema: z.ZodObject<{
4
+ dir: z.ZodOptional<z.ZodString>;
5
+ name: z.ZodString;
6
+ description: z.ZodOptional<z.ZodString>;
7
+ enablePowerhouse: z.ZodDefault<z.ZodBoolean>;
8
+ enableMastra: z.ZodDefault<z.ZodBoolean>;
9
+ enableRoutine: z.ZodDefault<z.ZodBoolean>;
10
+ force: z.ZodDefault<z.ZodBoolean>;
11
+ skipPhInit: z.ZodDefault<z.ZodBoolean>;
12
+ skipInstall: z.ZodDefault<z.ZodBoolean>;
13
+ }, z.core.$strip>;
14
+ type InitInput = z.output<typeof inputSchema>;
15
+ export declare function buildSpec(input: InitInput): ClintProjectSpec;
16
+ export declare const init: import("@powerhousedao/ph-clint").Command<z.ZodObject<{
17
+ dir: z.ZodOptional<z.ZodString>;
18
+ name: z.ZodString;
19
+ description: z.ZodOptional<z.ZodString>;
20
+ enablePowerhouse: z.ZodDefault<z.ZodBoolean>;
21
+ enableMastra: z.ZodDefault<z.ZodBoolean>;
22
+ enableRoutine: z.ZodDefault<z.ZodBoolean>;
23
+ force: z.ZodDefault<z.ZodBoolean>;
24
+ skipPhInit: z.ZodDefault<z.ZodBoolean>;
25
+ skipInstall: z.ZodDefault<z.ZodBoolean>;
26
+ }, z.core.$strip>, {
27
+ text: string;
28
+ data?: undefined;
29
+ } | {
30
+ text: string;
31
+ data: {
32
+ targetDir: string;
33
+ spec: {
34
+ name: string;
35
+ version: string;
36
+ description: string;
37
+ features: {
38
+ powerhouse: {
39
+ enabled: boolean;
40
+ switchboard: boolean;
41
+ connect: boolean;
42
+ };
43
+ mastra: {
44
+ enabled: boolean;
45
+ };
46
+ routine: {
47
+ enabled: boolean;
48
+ };
49
+ };
50
+ documentTypes: string[];
51
+ scope?: string | undefined;
52
+ bin?: string | undefined;
53
+ };
54
+ fileCount: number;
55
+ };
56
+ }, Record<string, unknown>>;
57
+ export {};
@@ -0,0 +1,168 @@
1
+ /**
2
+ * `ph-clint clint-project-init` — the project bootstrap wizard.
3
+ *
4
+ * Behavior:
5
+ * 1. Resolve target dir (positional `dir`, defaults to cwd).
6
+ * 2. Assert it is empty (or `--force`).
7
+ * 3. Collect project identity + feature toggles (prompt for missing).
8
+ * 4. Assemble a ClintProjectSpec and validate with Zod.
9
+ * 5. Write the spec + emit the generated tree via `generateProject`.
10
+ * 6. (Split layout) run `ph init` in `{name}-app/` unless `--skip-ph-init`.
11
+ * 7. Run `pnpm install` in generated sub-projects unless `--skip-install`.
12
+ * 8. Print next-steps guidance.
13
+ */
14
+ import path from 'node:path';
15
+ import { defineCommand } from '@powerhousedao/ph-clint';
16
+ import { z } from 'zod';
17
+ import { clintProjectSpecSchema, } from '../spec/types.js';
18
+ import { generateProject } from '../codegen/index.js';
19
+ import { isDirEmptyEnough } from '../codegen/write.js';
20
+ import { runPhInit, runPnpmInstall } from '../codegen/scaffold.js';
21
+ const inputSchema = z.object({
22
+ dir: z
23
+ .string()
24
+ .optional()
25
+ .describe('Target directory (defaults to the current directory)'),
26
+ name: z
27
+ .string()
28
+ .describe('Project name — bare, or `@scope/name`. Lowercase letters, digits, hyphens.'),
29
+ description: z.string().optional().describe('Short project description'),
30
+ enablePowerhouse: z
31
+ .boolean()
32
+ .default(false)
33
+ .describe('Enable Powerhouse reactor (requires split layout)'),
34
+ enableMastra: z
35
+ .boolean()
36
+ .default(false)
37
+ .describe('Enable Mastra AI agent stack'),
38
+ enableRoutine: z
39
+ .boolean()
40
+ .default(false)
41
+ .describe('Enable the tick-based routine loop'),
42
+ force: z
43
+ .boolean()
44
+ .default(false)
45
+ .describe('Overwrite files if the target directory is not empty'),
46
+ skipPhInit: z
47
+ .boolean()
48
+ .default(false)
49
+ .describe('Do not run `ph init` in the app folder after scaffolding'),
50
+ skipInstall: z
51
+ .boolean()
52
+ .default(false)
53
+ .describe('Do not run `pnpm install` after scaffolding'),
54
+ });
55
+ /** Parse `@scope/name` into { scope, name }. */
56
+ function splitPackageName(raw) {
57
+ const match = /^@([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/.exec(raw);
58
+ if (match)
59
+ return { scope: match[1], name: match[2] };
60
+ if (raw.startsWith('@')) {
61
+ throw new Error(`invalid package name "${raw}" — expected "@scope/name" with lowercase letters, digits, hyphens`);
62
+ }
63
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(raw)) {
64
+ throw new Error(`invalid project name "${raw}" — expected lowercase letters, digits, hyphens`);
65
+ }
66
+ return { name: raw };
67
+ }
68
+ export function buildSpec(input) {
69
+ const { scope, name } = splitPackageName(input.name);
70
+ // Routine is forced on when Mastra is on — the REPL-only path works, but
71
+ // the agent routine loop is what most users actually want.
72
+ const routine = input.enableRoutine || input.enableMastra;
73
+ const candidate = {
74
+ name,
75
+ scope,
76
+ description: input.description ?? '',
77
+ features: {
78
+ powerhouse: { enabled: input.enablePowerhouse },
79
+ mastra: { enabled: input.enableMastra },
80
+ routine: { enabled: routine },
81
+ },
82
+ };
83
+ return clintProjectSpecSchema.parse(candidate);
84
+ }
85
+ export const init = defineCommand({
86
+ id: 'clint-project-init',
87
+ description: 'Initialize a new ph-clint project in the target directory',
88
+ inputSchema,
89
+ prompt: {
90
+ promptForDefaults: true,
91
+ promptOptional: ['description'],
92
+ },
93
+ execute: async (input, { workdir, stdout }) => {
94
+ const targetDir = path.resolve(workdir, input.dir ?? '.');
95
+ if (!input.force) {
96
+ const empty = await isDirEmptyEnough(targetDir);
97
+ if (!empty) {
98
+ return {
99
+ text: `Error: ${targetDir} is not empty. Re-run with --force to initialize anyway,\n` +
100
+ 'or choose a different --dir.',
101
+ };
102
+ }
103
+ }
104
+ const spec = buildSpec(input);
105
+ const result = await generateProject({
106
+ targetDir,
107
+ spec,
108
+ mode: 'create',
109
+ allowNonEmpty: input.force,
110
+ });
111
+ stdout(`Generated ${result.files.length} files in ${targetDir}\n`);
112
+ const split = spec.features.powerhouse.enabled;
113
+ // Reactor package scaffolding (split layout only).
114
+ if (split && result.appDir && !input.skipPhInit) {
115
+ const phResult = await runPhInit({
116
+ targetDir,
117
+ appDir: result.appDir,
118
+ spec,
119
+ log: (msg) => stdout(msg + '\n'),
120
+ });
121
+ if (!phResult.ran) {
122
+ stdout(`Note: \`ph init\` did not run — ${phResult.reason ?? 'see above'}.\n`);
123
+ }
124
+ }
125
+ // Dependency installation.
126
+ if (!input.skipInstall) {
127
+ const dirs = [];
128
+ if (split) {
129
+ if (result.appDir)
130
+ dirs.push(result.appDir);
131
+ dirs.push(result.cliDir);
132
+ }
133
+ else {
134
+ dirs.push(targetDir);
135
+ }
136
+ await runPnpmInstall({
137
+ dirs,
138
+ log: (msg) => stdout(msg + '\n'),
139
+ });
140
+ }
141
+ const next = [];
142
+ next.push('');
143
+ next.push('Next steps:');
144
+ const relDir = path.relative(workdir, targetDir) || '.';
145
+ if (split) {
146
+ next.push(` cd ${relDir}`);
147
+ if (input.skipPhInit) {
148
+ next.push(` cd ${spec.name}-app && ph init . && cd ..`);
149
+ }
150
+ if (input.skipInstall) {
151
+ next.push(' pnpm install');
152
+ }
153
+ next.push(' pnpm dev');
154
+ }
155
+ else {
156
+ next.push(` cd ${relDir}`);
157
+ if (input.skipInstall)
158
+ next.push(' pnpm install');
159
+ next.push(' pnpm dev');
160
+ }
161
+ next.push('');
162
+ next.push('Edit the project spec and run `ph-clint clint-project-regen` to regenerate.');
163
+ return {
164
+ text: next.join('\n'),
165
+ data: { targetDir, spec, fileCount: result.files.length },
166
+ };
167
+ },
168
+ });
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ export declare const clintProjectPublish: import("@powerhousedao/ph-clint").Command<z.ZodObject<{
3
+ tag: z.ZodEnum<{
4
+ dev: "dev";
5
+ staging: "staging";
6
+ production: "production";
7
+ }>;
8
+ dir: z.ZodOptional<z.ZodString>;
9
+ dryRun: z.ZodDefault<z.ZodBoolean>;
10
+ skipBuild: z.ZodDefault<z.ZodBoolean>;
11
+ verbose: z.ZodDefault<z.ZodBoolean>;
12
+ }, z.core.$strip>, {
13
+ text: string;
14
+ data: import("@powerhousedao/ph-clint-dev/publish").PublishResult;
15
+ }, Record<string, unknown>>;
@@ -0,0 +1,71 @@
1
+ import path from 'node:path';
2
+ import { defineCommand } from '@powerhousedao/ph-clint';
3
+ import { z } from 'zod';
4
+ import { resolvePublishPlan, buildPackages, publishPackages, } from '@powerhousedao/ph-clint-dev/publish';
5
+ const inputSchema = z.object({
6
+ tag: z.enum(['dev', 'staging', 'production']).describe('Release channel'),
7
+ dir: z.string().optional().describe('Project root directory'),
8
+ dryRun: z.boolean().default(false).describe('Preview without publishing'),
9
+ skipBuild: z.boolean().default(false),
10
+ verbose: z.boolean().default(false),
11
+ });
12
+ function formatPlanPreview(plan) {
13
+ const lines = [];
14
+ const distTag = plan.tag === 'production' ? 'latest' : plan.tag;
15
+ lines.push(`Publish plan:`);
16
+ lines.push(` Group: ${plan.groupName}`);
17
+ lines.push(` Version: ${plan.version}`);
18
+ lines.push(` Tag: ${distTag}`);
19
+ lines.push(` Registry: ${plan.registry}`);
20
+ lines.push(` Packages:`);
21
+ for (const pkg of plan.packages) {
22
+ lines.push(` - ${pkg.name}`);
23
+ }
24
+ return lines.join('\n');
25
+ }
26
+ export const clintProjectPublish = defineCommand({
27
+ id: 'clint-project-publish',
28
+ description: 'Publish project packages to npm',
29
+ inputSchema,
30
+ execute: async (input, { workdir, stdout }) => {
31
+ const dir = path.resolve(input.dir ?? workdir);
32
+ const logLines = [];
33
+ const log = (msg) => {
34
+ logLines.push(msg);
35
+ stdout(msg + '\n');
36
+ };
37
+ // 1. Resolve plan
38
+ const plan = await resolvePublishPlan({
39
+ tag: input.tag,
40
+ configPath: undefined, // auto-discover from dir
41
+ skipGitCheck: input.dryRun,
42
+ log,
43
+ });
44
+ // 2. Show preview
45
+ stdout(formatPlanPreview(plan) + '\n');
46
+ // 3. Build
47
+ if (!input.skipBuild) {
48
+ await buildPackages(plan, { verbose: input.verbose, log });
49
+ }
50
+ // 4. Publish (or dry-run)
51
+ const result = await publishPackages(plan, {
52
+ dryRun: input.dryRun,
53
+ verbose: input.verbose,
54
+ log,
55
+ });
56
+ if (result.success) {
57
+ return {
58
+ text: result.dryRun
59
+ ? `Dry run complete — ${plan.packages.length} packages validated at ${result.version}.`
60
+ : `Published ${result.published.length} packages at ${result.version}.`,
61
+ data: result,
62
+ };
63
+ }
64
+ else {
65
+ return {
66
+ text: `Publish failed. Published: ${result.published.length}, Failed: ${result.failed.length}.`,
67
+ data: result,
68
+ };
69
+ }
70
+ },
71
+ });
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ export declare const regen: import("@powerhousedao/ph-clint").Command<z.ZodObject<{
3
+ dir: z.ZodOptional<z.ZodString>;
4
+ force: z.ZodDefault<z.ZodBoolean>;
5
+ }, z.core.$strip>, {
6
+ text: string;
7
+ data?: undefined;
8
+ } | {
9
+ text: string;
10
+ data: {
11
+ mode: "create" | "update";
12
+ files: string[];
13
+ skipped: string[];
14
+ deleted: string[];
15
+ migrated: boolean;
16
+ warnings: string[];
17
+ };
18
+ }, Record<string, unknown>>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * `ph-clint clint-project-regen` — reconcile an existing project against its persisted
3
+ * spec. Equivalent to running the generator in update mode.
4
+ *
5
+ * Run inside a project directory (or pass `--dir`). If the spec has flipped
6
+ * `features.powerhouse.enabled` since the last run, the flat → split
7
+ * migration fires first.
8
+ */
9
+ import path from 'node:path';
10
+ import { defineCommand } from '@powerhousedao/ph-clint';
11
+ import { z } from 'zod';
12
+ import { readProjectSpec } from '../spec/file.js';
13
+ import { generateProject } from '../codegen/index.js';
14
+ const inputSchema = z.object({
15
+ dir: z
16
+ .string()
17
+ .optional()
18
+ .describe('Project directory (defaults to the current directory)'),
19
+ force: z
20
+ .boolean()
21
+ .default(false)
22
+ .describe('Overwrite user-edited managed files and bypass the git-dirty guard'),
23
+ });
24
+ export const regen = defineCommand({
25
+ id: 'clint-project-regen',
26
+ description: 'Regenerate project files from the persisted spec',
27
+ inputSchema,
28
+ execute: async (input, { workdir, stdout, log }) => {
29
+ const targetDir = path.resolve(workdir, input.dir ?? '.');
30
+ const spec = await readProjectSpec(targetDir);
31
+ if (!spec) {
32
+ return {
33
+ text: `Error: no spec found at ${targetDir}/.ph/ph-clint-cli/project-spec.json.\n` +
34
+ 'Run `ph-clint clint-project-init` first to scaffold a new project.',
35
+ };
36
+ }
37
+ const warnings = [];
38
+ const result = await generateProject({
39
+ targetDir,
40
+ spec,
41
+ mode: 'update',
42
+ force: input.force,
43
+ onWarn: (m) => warnings.push(m),
44
+ });
45
+ const lines = [];
46
+ if (result.migrated)
47
+ lines.push('Migrated layout: flat → split.');
48
+ lines.push(`Wrote ${result.files.length} file(s); skipped ${result.skipped.length}; deleted ${result.deleted.length}.`);
49
+ for (const w of warnings) {
50
+ if (log)
51
+ log.warn(w);
52
+ else
53
+ stdout('warning: ' + w + '\n');
54
+ }
55
+ stdout(lines.join('\n') + '\n');
56
+ return {
57
+ text: lines.join('\n'),
58
+ data: {
59
+ mode: result.mode,
60
+ files: result.files.map((f) => f.relativePath),
61
+ skipped: result.skipped.map((f) => f.relativePath),
62
+ deleted: result.deleted.map((f) => f.relativePath),
63
+ migrated: result.migrated,
64
+ warnings,
65
+ },
66
+ };
67
+ },
68
+ });
package/dist/config.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- /** CLI name — used for config resolution, env var prefixing, and .ph/ paths. */
2
- export declare const CLI_NAME = "ph-clint";
3
- export declare const CLI_VERSION = "0.0.0";
4
- /** Project root — resolved from this file's location. */
5
- export declare const PROJECT_ROOT: string;
1
+ export declare const CLI_ROOT: string;
2
+ export declare const CLI_NAME: string;
3
+ export declare const CLI_VERSION: string;
package/dist/config.js CHANGED
@@ -1,7 +1,5 @@
1
- import path from 'node:path';
2
- import { fileURLToPath } from 'node:url';
3
- /** CLI name — used for config resolution, env var prefixing, and .ph/ paths. */
4
- export const CLI_NAME = 'ph-clint';
5
- export const CLI_VERSION = '0.0.0';
6
- /** Project root — resolved from this file's location. */
7
- export const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
1
+ import { readPackageInfo } from '@powerhousedao/ph-clint';
2
+ const pkg = readPackageInfo(import.meta.url);
3
+ export const CLI_ROOT = pkg.root;
4
+ export const CLI_NAME = pkg.name.replace(/-cli$/, '');
5
+ export const CLI_VERSION = pkg.version;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * THIS FILE IS GENERATED BY ph-clint-cli. DO NOT EDIT.
3
3
  *
4
- * Regenerated on every `ph-clint regen` and on every spec-change tick.
4
+ * Regenerated on every `ph-clint clint-project-regen` and on every spec-change tick.
5
5
  * Put custom bindings in `framework.ts`, not here — edits to this file
6
6
  * will be overwritten.
7
7
  */
@@ -0,0 +1 @@
1
+ export declare const clintProject: import("@powerhousedao/ph-clint").ServiceDefinition<Record<string, unknown>>;
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { defineService, checkWorkdir } from '@powerhousedao/ph-clint';
4
+ export const clintProject = defineService({
5
+ id: 'clint-project',
6
+ name: 'Clint Project',
7
+ description: 'Development server for a ph-clint implementation project',
8
+ command: () => 'pnpm dev',
9
+ readiness: {
10
+ patterns: [{
11
+ name: 'cli-ready',
12
+ pattern: /Interactive mode started|ready/i,
13
+ captures: {},
14
+ }],
15
+ timeout: 30_000,
16
+ },
17
+ preflight: [
18
+ checkWorkdir((cwd) => {
19
+ const pkg = path.join(cwd, 'package.json');
20
+ if (!fs.existsSync(pkg))
21
+ return false;
22
+ try {
23
+ const json = JSON.parse(fs.readFileSync(pkg, 'utf8'));
24
+ return !!json.dependencies?.['@powerhousedao/ph-clint']
25
+ || !!json.scripts?.dev;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }, 'Not a ph-clint project directory', 'Use --workdir to point to a ph-clint project root'),
31
+ ],
32
+ projectScanner: {
33
+ isProjectFolder: (p) => {
34
+ const pkg = path.join(p, 'package.json');
35
+ if (!fs.existsSync(pkg))
36
+ return false;
37
+ try {
38
+ const json = JSON.parse(fs.readFileSync(pkg, 'utf8'));
39
+ return !!json.dependencies?.['@powerhousedao/ph-clint'];
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ },
45
+ },
46
+ restart: { enabled: false, maxRetries: 0, delay: 0 },
47
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powerhousedao/ph-clint-cli",
3
- "version": "0.1.0-dev.0",
3
+ "version": "0.1.0-dev.10",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -22,12 +22,11 @@
22
22
  "lint": "eslint src",
23
23
  "mastra:dev": "mastra dev",
24
24
  "mastra:build": "mastra build",
25
- "mastra:start": "mastra start",
26
- "publish:npm": "pnpm build && pnpm publish --no-git-checks"
25
+ "mastra:start": "mastra start"
27
26
  },
28
27
  "dependencies": {
29
- "@powerhousedao/ph-clint": "^0.1.0-dev.0",
30
- "@powerhousedao/ph-clint-app": "^0.1.0-dev.0",
28
+ "@powerhousedao/ph-clint": "^0.1.0-dev.10",
29
+ "@powerhousedao/ph-clint-app": "^0.1.0-dev.10",
31
30
  "@mastra/core": "^1.22.0",
32
31
  "@mastra/libsql": "^1.7.4",
33
32
  "@mastra/memory": "^1.13.1",
@@ -38,6 +37,7 @@
38
37
  "document-model": "6.0.0-dev.170",
39
38
  "kysely": "^0.27.0",
40
39
  "kysely-pglite-dialect": "^1.3.0",
40
+ "@powerhousedao/ph-clint-dev": "^0.1.0-dev.10",
41
41
  "zod": "^4.3.6"
42
42
  },
43
43
  "pnpm": {
@@ -49,7 +49,6 @@
49
49
  "@jest/globals": "^30.2.0",
50
50
  "@types/node": "^25.5.2",
51
51
  "jest": "^30.2.0",
52
- "@powerhousedao/ph-clint-dev": "^0.1.0-dev.0",
53
52
  "ts-jest": "^29.4.6",
54
53
  "tsx": "^4.19.0",
55
54
  "typescript": "^6.0.2"