@powerhousedao/ph-clint-cli 0.1.0-dev.7 → 0.1.0-dev.71
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 +52 -10
- package/dist/codegen/actions.d.ts +1 -0
- package/dist/codegen/actions.js +1 -0
- package/dist/codegen/builders/agent-ts.d.ts +4 -4
- package/dist/codegen/builders/agent-ts.js +92 -2
- package/dist/codegen/builders/app-index-ts.d.ts +3 -10
- package/dist/codegen/builders/app-index-ts.js +30 -20
- package/dist/codegen/builders/cli-package-json.d.ts +1 -8
- package/dist/codegen/builders/cli-package-json.js +1 -96
- package/dist/codegen/builders/cli-ts.js +88 -37
- package/dist/codegen/builders/config-ts.d.ts +1 -1
- package/dist/codegen/builders/config-ts.js +2 -8
- package/dist/codegen/builders/demo-agent-ts.d.ts +8 -0
- package/dist/codegen/builders/demo-agent-ts.js +50 -0
- package/dist/codegen/builders/eslint-config-js.d.ts +1 -1
- package/dist/codegen/builders/eslint-config-js.js +7 -4
- package/dist/codegen/builders/framework-gen-ts.d.ts +11 -6
- package/dist/codegen/builders/framework-gen-ts.js +73 -21
- package/dist/codegen/builders/framework-ts.js +33 -7
- package/dist/codegen/builders/index.d.ts +1 -44
- package/dist/codegen/builders/index.js +1 -61
- package/dist/codegen/builders/manifest-json.d.ts +6 -0
- package/dist/codegen/builders/manifest-json.js +31 -0
- package/dist/codegen/builders/mastra-index-ts.d.ts +3 -0
- package/dist/codegen/builders/mastra-index-ts.js +47 -5
- package/dist/codegen/builders/publish-config-js.d.ts +8 -0
- package/dist/codegen/builders/publish-config-js.js +33 -0
- package/dist/codegen/builders/publish-config-ts.d.ts +4 -2
- package/dist/codegen/builders/publish-config-ts.js +25 -11
- package/dist/codegen/builders/readme-md.js +7 -6
- package/dist/codegen/builders/root-package-json.js +14 -11
- package/dist/codegen/builders/smoke-test-ts.d.ts +2 -0
- package/dist/codegen/builders/smoke-test-ts.js +15 -0
- package/dist/codegen/exec.d.ts +1 -20
- package/dist/codegen/exec.js +1 -41
- package/dist/codegen/generated.d.ts +1 -0
- package/dist/codegen/generated.js +1 -0
- package/dist/codegen/hashes.d.ts +1 -6
- package/dist/codegen/hashes.js +1 -60
- package/dist/codegen/index.d.ts +8 -36
- package/dist/codegen/index.js +6 -293
- package/dist/codegen/markers.d.ts +1 -36
- package/dist/codegen/markers.js +1 -86
- package/dist/codegen/scaffold.d.ts +1 -34
- package/dist/codegen/scaffold.js +1 -60
- package/dist/codegen/write.d.ts +1 -6
- package/dist/codegen/write.js +1 -32
- package/dist/commands/clint-project-build.d.ts +63 -1
- package/dist/commands/clint-project-build.js +11 -28
- package/dist/commands/clint-project-init.d.ts +100 -10
- package/dist/commands/clint-project-init.js +63 -47
- package/dist/commands/clint-project-publish.d.ts +64 -1
- package/dist/commands/clint-project-publish.js +6 -4
- package/dist/commands/clint-project-regen.d.ts +63 -1
- package/dist/commands/clint-project-regen.js +26 -2
- package/dist/commands/clint-skills-sync.d.ts +72 -0
- package/dist/commands/clint-skills-sync.js +56 -0
- package/dist/framework.d.ts +228 -50
- package/dist/framework.gen.d.ts +34 -8
- package/dist/framework.js +1 -5
- package/{prompts/agent-profiles/AgentBase.md → dist/gen/agent-profiles/PhClintDevAgent.md} +1 -1
- package/dist/gen/skills/cli-setup/.cli-docs.md +135 -0
- package/dist/gen/skills/cli-setup/SKILL.md +125 -0
- package/dist/gen/skills/cli-setup/references/00.assess-cli-needs.md +53 -0
- package/dist/gen/skills/cli-setup/references/01.define-config-schema.md +78 -0
- package/dist/gen/skills/cli-setup/references/02.wire-cli.md +141 -0
- package/dist/gen/skills/cli-setup/references/03.add-entrypoint.md +128 -0
- package/dist/gen/skills/cli-setup/references/expected-outcome.md +3 -0
- package/dist/gen/skills/command-definition/.cli-docs.md +84 -0
- package/dist/gen/skills/command-definition/SKILL.md +99 -0
- package/dist/gen/skills/command-definition/references/00.assess-requirements.md +58 -0
- package/dist/gen/skills/command-definition/references/01.define-schema.md +72 -0
- package/dist/gen/skills/command-definition/references/02.implement-execute.md +118 -0
- package/dist/gen/skills/command-definition/references/03.add-prompting.md +54 -0
- package/dist/gen/skills/command-definition/references/expected-outcome.md +3 -0
- package/dist/gen/skills/service-definition/.cli-docs.md +214 -0
- package/dist/gen/skills/service-definition/SKILL.md +148 -0
- package/dist/gen/skills/service-definition/references/00.assess-service.md +69 -0
- package/dist/gen/skills/service-definition/references/01.define-service.md +166 -0
- package/dist/gen/skills/service-definition/references/02.add-preflight.md +132 -0
- package/dist/gen/skills/service-definition/references/03.add-project-scanner.md +82 -0
- package/dist/gen/skills/service-definition/references/04.wire-events.md +120 -0
- package/dist/gen/skills/service-definition/references/expected-outcome.md +3 -0
- package/dist/services/clint-project.js +21 -0
- package/dist/skills/sync.d.ts +1 -0
- package/dist/skills/sync.js +1 -0
- package/dist/spec/ensure-document.d.ts +17 -0
- package/dist/spec/ensure-document.js +56 -0
- package/dist/spec/file.d.ts +1 -4
- package/dist/spec/file.js +1 -34
- package/dist/spec/from-document.d.ts +4 -1
- package/dist/spec/from-document.js +4 -16
- package/dist/spec/types.d.ts +1 -62
- package/dist/spec/types.js +1 -112
- package/dist/triggers/publish-trigger.d.ts +67 -0
- package/dist/triggers/publish-trigger.js +137 -0
- package/dist/triggers/spec-change.d.ts +46 -18
- package/dist/triggers/spec-change.js +138 -102
- package/package.json +12 -17
- package/prompts/skills-ext/.gitkeep +0 -0
- package/prompts/skills-tpl/.gitkeep +0 -0
|
@@ -1,18 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the root `publish.config.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Split-layout: a lockstep group with both app and cli packages.
|
|
5
|
+
* Single-layout: a single-package group at `.`.
|
|
6
|
+
*/
|
|
7
|
+
import { phAtLeast } from '../../spec/types.js';
|
|
1
8
|
export function buildPublishConfigTs(spec) {
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
const split = phAtLeast(spec.features.powerhouse, 'Reactor');
|
|
10
|
+
// The publish pipeline appends the prerelease suffix (e.g. -dev.0), so we
|
|
11
|
+
// only store the base semver (M.m.p) here.
|
|
12
|
+
const baseVersion = spec.version.replace(/-.*$/, '');
|
|
13
|
+
const packages = split
|
|
14
|
+
? `[
|
|
15
|
+
{ path: '${spec.name}-app', category: 'app' },
|
|
16
|
+
{ path: '${spec.name}-cli', category: 'cli' },
|
|
17
|
+
]`
|
|
18
|
+
: `[
|
|
19
|
+
{ path: '.', category: 'cli' },
|
|
20
|
+
]`;
|
|
21
|
+
return `// Publish config — loaded by ph-clint's publish pipeline.
|
|
22
|
+
// definePublishConfig is an identity function; a plain export is equivalent.
|
|
23
|
+
export default {
|
|
7
24
|
groups: {
|
|
8
25
|
'${spec.name}': {
|
|
9
|
-
version: '${
|
|
10
|
-
packages:
|
|
11
|
-
{ path: '${app}', category: 'app' },
|
|
12
|
-
{ path: '${cli}', category: 'cli' },
|
|
13
|
-
],
|
|
26
|
+
version: '${baseVersion}',
|
|
27
|
+
packages: ${packages},
|
|
14
28
|
},
|
|
15
29
|
},
|
|
16
|
-
}
|
|
30
|
+
};
|
|
17
31
|
`;
|
|
18
32
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Builds the project README for a freshly-generated ph-clint project.
|
|
3
3
|
*/
|
|
4
|
-
import { getPackageName, } from '../../spec/types.js';
|
|
4
|
+
import { getPackageName, phAtLeast, } from '../../spec/types.js';
|
|
5
5
|
export function buildReadme(spec) {
|
|
6
6
|
const { mastra, routine, powerhouse } = spec.features;
|
|
7
|
+
const ph = powerhouse;
|
|
7
8
|
const title = getPackageName(spec);
|
|
8
9
|
const lines = [];
|
|
9
10
|
lines.push(`# ${title}`);
|
|
@@ -19,10 +20,10 @@ export function buildReadme(spec) {
|
|
|
19
20
|
lines.push('');
|
|
20
21
|
lines.push('## Enabled features');
|
|
21
22
|
lines.push('');
|
|
22
|
-
lines.push(`- **Powerhouse**: ${
|
|
23
|
-
if (
|
|
24
|
-
lines.push(` - Switchboard: ${
|
|
25
|
-
lines.push(` - Connect: ${
|
|
23
|
+
lines.push(`- **Powerhouse**: ${ph !== 'Disabled' ? `on (${ph})` : 'off'}`);
|
|
24
|
+
if (phAtLeast(ph, 'Reactor')) {
|
|
25
|
+
lines.push(` - Switchboard: ${phAtLeast(ph, 'Switchboard') ? 'on' : 'off'}`);
|
|
26
|
+
lines.push(` - Connect: ${phAtLeast(ph, 'Connect') ? 'on' : 'off'}`);
|
|
26
27
|
}
|
|
27
28
|
lines.push(`- **Mastra agent**: ${mastra.enabled ? 'on' : 'off'}`);
|
|
28
29
|
lines.push(`- **Routine loop**: ${routine.enabled ? 'on' : 'off'}`);
|
|
@@ -32,7 +33,7 @@ export function buildReadme(spec) {
|
|
|
32
33
|
lines.push('Toggle features or update metadata in `.ph/ph-clint-cli/project-spec.json`,');
|
|
33
34
|
lines.push('then re-run `ph-clint clint-project-regen` to regenerate.');
|
|
34
35
|
lines.push('');
|
|
35
|
-
if (
|
|
36
|
+
if (phAtLeast(ph, 'Reactor')) {
|
|
36
37
|
lines.push('## Split layout');
|
|
37
38
|
lines.push('');
|
|
38
39
|
lines.push(`This project is split into \`${spec.name}-cli/\` (the CLI) and \`${spec.name}-app/\` (the Powerhouse reactor package).`);
|
|
@@ -6,29 +6,32 @@
|
|
|
6
6
|
* Uses `pnpm --prefix <dir>` for all orchestration scripts — no cwd mutation.
|
|
7
7
|
* Build order: app first, then cli (cli depends on app via `file:` dep).
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { getAppFolderName, getBinName, getCliFolderName, phAtLeast, } from '../../spec/types.js';
|
|
10
10
|
export function buildRootPackageJson(spec) {
|
|
11
|
-
const cli =
|
|
12
|
-
const app =
|
|
11
|
+
const cli = getCliFolderName(spec);
|
|
12
|
+
const app = getAppFolderName(spec);
|
|
13
13
|
const prefix = (dir, cmd) => `pnpm --prefix ${dir} ${cmd}`;
|
|
14
14
|
const both = (cmd) => `${prefix(app, cmd)} && ${prefix(cli, cmd)}`;
|
|
15
|
+
const connectEnabled = phAtLeast(spec.features.powerhouse, 'Connect');
|
|
16
|
+
const buildScript = connectEnabled
|
|
17
|
+
? `${prefix(app, 'build')} && ${prefix(app, 'connect build --outDir dist/connect')} && ${prefix(cli, 'build')}`
|
|
18
|
+
: both('build');
|
|
15
19
|
const scripts = {
|
|
16
20
|
install: both('install'),
|
|
17
|
-
build:
|
|
21
|
+
build: buildScript,
|
|
18
22
|
test: both('test'),
|
|
19
23
|
dev: prefix(cli, 'dev'),
|
|
20
24
|
start: prefix(cli, 'start'),
|
|
21
|
-
lint:
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'publish:
|
|
25
|
-
'publish:staging': `${prefix(cli, 'exec ph-publish')} staging -c ./publish.config.ts`,
|
|
26
|
-
'publish:production': `${prefix(cli, 'exec ph-publish')} production -c ./publish.config.ts`,
|
|
25
|
+
lint: both('lint'),
|
|
26
|
+
'publish:dev': `${prefix(cli, 'exec ph-publish')} dev -c ../publish.config.js${connectEnabled ? ' --verify-connect' : ''}`,
|
|
27
|
+
'publish:staging': `${prefix(cli, 'exec ph-publish')} staging -c ../publish.config.js${connectEnabled ? ' --verify-connect' : ''}`,
|
|
28
|
+
'publish:production': `${prefix(cli, 'exec ph-publish')} production -c ../publish.config.js${connectEnabled ? ' --verify-connect' : ''}`,
|
|
27
29
|
};
|
|
28
30
|
const pkg = {
|
|
29
|
-
name:
|
|
31
|
+
name: spec.scope ? `${spec.scope}/${getBinName(spec)}` : getBinName(spec),
|
|
30
32
|
version: spec.version,
|
|
31
33
|
private: true,
|
|
34
|
+
type: 'module',
|
|
32
35
|
...(spec.description ? { description: spec.description } : {}),
|
|
33
36
|
scripts,
|
|
34
37
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getBinName } from '../../spec/types.js';
|
|
2
|
+
export function buildSmokeTestTs(spec) {
|
|
3
|
+
const bin = getBinName(spec);
|
|
4
|
+
return [
|
|
5
|
+
`import { describe, it, expect } from '@jest/globals';`,
|
|
6
|
+
``,
|
|
7
|
+
`describe('${bin}', () => {`,
|
|
8
|
+
` it('loads without error', async () => {`,
|
|
9
|
+
` const { cli } = await import('../src/cli.js');`,
|
|
10
|
+
` expect(cli).toBeDefined();`,
|
|
11
|
+
` });`,
|
|
12
|
+
`});`,
|
|
13
|
+
``,
|
|
14
|
+
].join('\n');
|
|
15
|
+
}
|
package/dist/codegen/exec.d.ts
CHANGED
|
@@ -1,20 +1 @@
|
|
|
1
|
-
export
|
|
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
|
-
/** Returns true if `command` is on PATH. */
|
|
20
|
-
export declare function hasCommandOnPath(command: string): Promise<boolean>;
|
|
1
|
+
export { hasCommandOnPath, getPhVersion, } from '@powerhousedao/ph-clint-dev/codegen/exec';
|
package/dist/codegen/exec.js
CHANGED
|
@@ -1,41 +1 @@
|
|
|
1
|
-
|
|
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
|
-
*/
|
|
9
|
-
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
|
-
/** Returns true if `command` is on PATH. */
|
|
33
|
-
export async function hasCommandOnPath(command) {
|
|
34
|
-
return new Promise((resolve) => {
|
|
35
|
-
const child = spawn(process.platform === 'win32' ? 'where' : 'which', [command], {
|
|
36
|
-
stdio: 'ignore',
|
|
37
|
-
});
|
|
38
|
-
child.once('error', () => resolve(false));
|
|
39
|
-
child.once('close', (code) => resolve(code === 0));
|
|
40
|
-
});
|
|
41
|
-
}
|
|
1
|
+
export { hasCommandOnPath, getPhVersion, } from '@powerhousedao/ph-clint-dev/codegen/exec';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type GeneratedState, getGeneratedPath, readGeneratedState, writeGeneratedState, generatedStateFromSpec, } from '@powerhousedao/ph-clint-dev/codegen/generated';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getGeneratedPath, readGeneratedState, writeGeneratedState, generatedStateFromSpec, } from '@powerhousedao/ph-clint-dev/codegen/generated';
|
package/dist/codegen/hashes.d.ts
CHANGED
|
@@ -1,6 +1 @@
|
|
|
1
|
-
export type HashRecord
|
|
2
|
-
export declare function getHashesPath(targetDir: string): string;
|
|
3
|
-
export declare function hashContent(content: string): string;
|
|
4
|
-
export declare function hashFile(filePath: string): Promise<string | null>;
|
|
5
|
-
export declare function readHashes(targetDir: string): Promise<HashRecord>;
|
|
6
|
-
export declare function writeHashes(targetDir: string, hashes: HashRecord): Promise<void>;
|
|
1
|
+
export { type HashRecord, getHashesPath, hashContent, hashFile, readHashes, writeHashes, } from '@powerhousedao/ph-clint-dev/codegen/hashes';
|
package/dist/codegen/hashes.js
CHANGED
|
@@ -1,60 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Per-file content hashes written by the generator so subsequent runs can
|
|
3
|
-
* detect whether a previously-generated file has been hand-edited.
|
|
4
|
-
*
|
|
5
|
-
* Stored at `{targetDir}/.ph/ph-clint-cli/.hashes.json` alongside the project
|
|
6
|
-
* spec. Keys are paths relative to the project root.
|
|
7
|
-
*/
|
|
8
|
-
import crypto from 'node:crypto';
|
|
9
|
-
import fs from 'node:fs/promises';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
const HASHES_DIR = path.join('.ph', 'ph-clint-cli');
|
|
12
|
-
const HASHES_FILE = '.hashes.json';
|
|
13
|
-
export function getHashesPath(targetDir) {
|
|
14
|
-
return path.join(targetDir, HASHES_DIR, HASHES_FILE);
|
|
15
|
-
}
|
|
16
|
-
export function hashContent(content) {
|
|
17
|
-
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
18
|
-
}
|
|
19
|
-
function isEnoent(err) {
|
|
20
|
-
return (!!err &&
|
|
21
|
-
typeof err === 'object' &&
|
|
22
|
-
'code' in err &&
|
|
23
|
-
err.code === 'ENOENT');
|
|
24
|
-
}
|
|
25
|
-
export async function hashFile(filePath) {
|
|
26
|
-
try {
|
|
27
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
28
|
-
return hashContent(content);
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
if (isEnoent(err))
|
|
32
|
-
return null;
|
|
33
|
-
throw err;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export async function readHashes(targetDir) {
|
|
37
|
-
try {
|
|
38
|
-
const raw = await fs.readFile(getHashesPath(targetDir), 'utf8');
|
|
39
|
-
const parsed = JSON.parse(raw);
|
|
40
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
41
|
-
return parsed;
|
|
42
|
-
}
|
|
43
|
-
return {};
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
if (isEnoent(err))
|
|
47
|
-
return {};
|
|
48
|
-
throw err;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
export async function writeHashes(targetDir, hashes) {
|
|
52
|
-
const file = getHashesPath(targetDir);
|
|
53
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
54
|
-
// Sort keys for stable output across runs.
|
|
55
|
-
const sorted = {};
|
|
56
|
-
for (const key of Object.keys(hashes).sort()) {
|
|
57
|
-
sorted[key] = hashes[key];
|
|
58
|
-
}
|
|
59
|
-
await fs.writeFile(file, JSON.stringify(sorted, null, 2) + '\n', 'utf8');
|
|
60
|
-
}
|
|
1
|
+
export { getHashesPath, hashContent, hashFile, readHashes, writeHashes, } from '@powerhousedao/ph-clint-dev/codegen/hashes';
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -1,39 +1,11 @@
|
|
|
1
|
-
import { type ClintProjectSpec } from '../spec/types.js';
|
|
2
|
-
export type GenerateMode = 'create' | 'update' | 'auto';
|
|
3
|
-
export interface GenerateProjectOptions {
|
|
4
|
-
/** Absolute path to the project root. */
|
|
5
|
-
targetDir: string;
|
|
6
|
-
spec: ClintProjectSpec;
|
|
7
|
-
/** Defaults to `'auto'` — create if no persisted spec, else update. */
|
|
8
|
-
mode?: GenerateMode;
|
|
9
|
-
/** Create mode: allow writing into a non-empty directory. */
|
|
10
|
-
allowNonEmpty?: boolean;
|
|
11
|
-
/**
|
|
12
|
-
* Update mode: overwrite user-edited files (hash-mismatch) anyway.
|
|
13
|
-
* Also relaxes the git-dirty guard on flat → split migration.
|
|
14
|
-
*/
|
|
15
|
-
force?: boolean;
|
|
16
|
-
/** Optional warning sink (diagnostics, not errors). */
|
|
17
|
-
onWarn?: (msg: string) => void;
|
|
18
|
-
}
|
|
19
|
-
export interface GeneratedFile {
|
|
20
|
-
absolutePath: string;
|
|
21
|
-
relativePath: string;
|
|
22
|
-
}
|
|
23
|
-
export interface GenerateProjectResult {
|
|
24
|
-
mode: 'create' | 'update';
|
|
25
|
-
/** Files newly written or rewritten. */
|
|
26
|
-
files: GeneratedFile[];
|
|
27
|
-
/** Files skipped because the user had edited them. */
|
|
28
|
-
skipped: GeneratedFile[];
|
|
29
|
-
/** Files removed because the new spec no longer emits them. */
|
|
30
|
-
deleted: GeneratedFile[];
|
|
31
|
-
/** True if a flat → split migration ran. */
|
|
32
|
-
migrated: boolean;
|
|
33
|
-
cliDir: string;
|
|
34
|
-
appDir: string | null;
|
|
35
|
-
}
|
|
36
1
|
/**
|
|
37
|
-
*
|
|
2
|
+
* Re-exports from @powerhousedao/ph-clint-dev/codegen with CLI_VERSION default.
|
|
38
3
|
*/
|
|
4
|
+
import { type GenerateProjectOptions as DevGenerateProjectOptions, type GenerateProjectResult, type GenerateMode, type GeneratedFile } from '@powerhousedao/ph-clint-dev/codegen';
|
|
5
|
+
import type { CodegenContext } from '@powerhousedao/ph-clint-dev/codegen/types';
|
|
6
|
+
export type { GenerateProjectResult, GenerateMode, GeneratedFile, CodegenContext };
|
|
7
|
+
export interface GenerateProjectOptions extends Omit<DevGenerateProjectOptions, 'context'> {
|
|
8
|
+
/** Codegen context; defaults to CLI_VERSION when omitted. */
|
|
9
|
+
context?: CodegenContext;
|
|
10
|
+
}
|
|
39
11
|
export declare function generateProject(options: GenerateProjectOptions): Promise<GenerateProjectResult>;
|
package/dist/codegen/index.js
CHANGED
|
@@ -1,298 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Takes a `ClintProjectSpec` + target directory and reconciles the on-disk
|
|
5
|
-
* project tree with it.
|
|
6
|
-
*
|
|
7
|
-
* Two modes:
|
|
8
|
-
*
|
|
9
|
-
* - **create** — the target dir is empty (or only contains ignorable
|
|
10
|
-
* artefacts). Every templated file is written fresh. Refuses to
|
|
11
|
-
* overwrite existing non-empty state unless `allowNonEmpty`.
|
|
12
|
-
*
|
|
13
|
-
* - **update** — a previous run's spec exists at
|
|
14
|
-
* `.ph/ph-clint-cli/project-spec.json`. Re-runs the builders, then for
|
|
15
|
-
* each emitted file:
|
|
16
|
-
* - files containing `@clint:begin/@clint:end` markers (today: just
|
|
17
|
-
* `src/cli.ts`) have their marker regions spliced in-place, leaving
|
|
18
|
-
* user code outside markers untouched;
|
|
19
|
-
* - files without markers are overwritten iff their on-disk hash still
|
|
20
|
-
* matches the hash stored from the previous write (user-edited files
|
|
21
|
-
* are skipped and surfaced via `onWarn`);
|
|
22
|
-
* - files the new spec no longer emits are removed if their hash still
|
|
23
|
-
* matches.
|
|
24
|
-
*
|
|
25
|
-
* A spec flip of `features.powerhouse.enabled` from `false` to `true`
|
|
26
|
-
* triggers the flat → split migration before the update flow runs.
|
|
27
|
-
*
|
|
28
|
-
* Mode is auto-detected by the presence of the persisted spec.
|
|
29
|
-
*/
|
|
30
|
-
import fs from 'node:fs/promises';
|
|
31
|
-
import path from 'node:path';
|
|
32
|
-
import { getAppFolderName, getCliFolderName, } from '../spec/types.js';
|
|
33
|
-
import { getSpecPath, readProjectSpec, writeProjectSpec, } from '../spec/file.js';
|
|
34
|
-
import { hashContent, hashFile, readHashes, writeHashes, } from './hashes.js';
|
|
35
|
-
import { hasMarkers, spliceMarkerRegions } from './markers.js';
|
|
36
|
-
import { writeFileEnsuringDir, isDirEmptyEnough } from './write.js';
|
|
37
|
-
import { migrateFlatToSplit } from './migrate/flat-to-split.js';
|
|
38
|
-
import { CLI_FILE_BUILDERS, buildReadme, buildRootPackageJson, buildPublishConfigTs, buildAppIndexTs, } from './builders/index.js';
|
|
39
|
-
/**
|
|
40
|
-
* Generate / reconcile a project at `targetDir` from the given spec.
|
|
2
|
+
* Re-exports from @powerhousedao/ph-clint-dev/codegen with CLI_VERSION default.
|
|
41
3
|
*/
|
|
4
|
+
import { generateProject as _generateProject, } from '@powerhousedao/ph-clint-dev/codegen';
|
|
5
|
+
import { CLI_VERSION } from '../config.js';
|
|
42
6
|
export async function generateProject(options) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const resolvedMode = resolveMode(options.mode ?? 'auto', existingSpec !== null);
|
|
47
|
-
// Flat → split migration, if needed. Runs BEFORE we compute planned files
|
|
48
|
-
// so the hash record and on-disk state are already in split-layout shape
|
|
49
|
-
// by the time the normal reconciliation runs.
|
|
50
|
-
let migrated = false;
|
|
51
|
-
if (resolvedMode === 'update' &&
|
|
52
|
-
existingSpec &&
|
|
53
|
-
!existingSpec.features.powerhouse.enabled &&
|
|
54
|
-
spec.features.powerhouse.enabled) {
|
|
55
|
-
await migrateFlatToSplit({
|
|
56
|
-
targetDir,
|
|
57
|
-
spec,
|
|
58
|
-
force: options.force,
|
|
59
|
-
onWarn: warn,
|
|
60
|
-
});
|
|
61
|
-
migrated = true;
|
|
62
|
-
}
|
|
63
|
-
// Dispatch to the mode-specific flow.
|
|
64
|
-
if (resolvedMode === 'create') {
|
|
65
|
-
return runCreate(options, warn);
|
|
66
|
-
}
|
|
67
|
-
return runUpdate(options, warn, migrated);
|
|
68
|
-
}
|
|
69
|
-
function resolveMode(requested, specExists) {
|
|
70
|
-
if (requested === 'create')
|
|
71
|
-
return 'create';
|
|
72
|
-
if (requested === 'update')
|
|
73
|
-
return 'update';
|
|
74
|
-
return specExists ? 'update' : 'create';
|
|
75
|
-
}
|
|
76
|
-
/** Build the list of files the new spec would emit, with their content. */
|
|
77
|
-
function planFiles(spec, targetDir) {
|
|
78
|
-
const split = spec.features.powerhouse.enabled;
|
|
79
|
-
const cliDir = split ? path.join(targetDir, getCliFolderName(spec)) : targetDir;
|
|
80
|
-
const appDir = split ? path.join(targetDir, getAppFolderName(spec)) : null;
|
|
81
|
-
const planned = [];
|
|
82
|
-
for (const builder of CLI_FILE_BUILDERS) {
|
|
83
|
-
const content = builder.build(spec);
|
|
84
|
-
if (content === null)
|
|
85
|
-
continue;
|
|
86
|
-
const abs = path.join(cliDir, builder.relativePath);
|
|
87
|
-
planned.push({
|
|
88
|
-
relativePath: path.relative(targetDir, abs),
|
|
89
|
-
absolutePath: abs,
|
|
90
|
-
content,
|
|
91
|
-
initOnly: builder.initOnly,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
// Project-root README — always at the top level.
|
|
95
|
-
const readmeAbs = path.join(targetDir, 'README.md');
|
|
96
|
-
planned.push({
|
|
97
|
-
relativePath: 'README.md',
|
|
98
|
-
absolutePath: readmeAbs,
|
|
99
|
-
content: buildReadme(spec),
|
|
7
|
+
return _generateProject({
|
|
8
|
+
...options,
|
|
9
|
+
context: options.context ?? { toolVersion: CLI_VERSION },
|
|
100
10
|
});
|
|
101
|
-
// Split-layout only: root package.json + app placeholder files.
|
|
102
|
-
if (split && appDir) {
|
|
103
|
-
const rootPkg = path.join(targetDir, 'package.json');
|
|
104
|
-
planned.push({
|
|
105
|
-
relativePath: 'package.json',
|
|
106
|
-
absolutePath: rootPkg,
|
|
107
|
-
content: buildRootPackageJson(spec),
|
|
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
|
-
});
|
|
115
|
-
const appGitkeep = path.join(appDir, '.gitkeep');
|
|
116
|
-
planned.push({
|
|
117
|
-
relativePath: path.relative(targetDir, appGitkeep),
|
|
118
|
-
absolutePath: appGitkeep,
|
|
119
|
-
content: '',
|
|
120
|
-
});
|
|
121
|
-
const appReadme = path.join(appDir, 'README.md');
|
|
122
|
-
planned.push({
|
|
123
|
-
relativePath: path.relative(targetDir, appReadme),
|
|
124
|
-
absolutePath: appReadme,
|
|
125
|
-
content: buildAppReadmeContent(spec),
|
|
126
|
-
});
|
|
127
|
-
// Top-level barrel for the reactor package — lets impl code import from
|
|
128
|
-
// `{name}-app` instead of drilling into `document-models/<slug>/v1/gen`.
|
|
129
|
-
const appIndex = buildAppIndexTs(spec);
|
|
130
|
-
if (appIndex !== null) {
|
|
131
|
-
const appIndexAbs = path.join(appDir, 'index.ts');
|
|
132
|
-
planned.push({
|
|
133
|
-
relativePath: path.relative(targetDir, appIndexAbs),
|
|
134
|
-
absolutePath: appIndexAbs,
|
|
135
|
-
content: appIndex,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return { planned, cliDir, appDir };
|
|
140
|
-
}
|
|
141
|
-
function buildAppReadmeContent(spec) {
|
|
142
|
-
return [
|
|
143
|
-
`# ${spec.name}-app`,
|
|
144
|
-
'',
|
|
145
|
-
'Powerhouse reactor-package for this project. Scaffold it with:',
|
|
146
|
-
'',
|
|
147
|
-
'```sh',
|
|
148
|
-
`cd ${spec.name}-app`,
|
|
149
|
-
'ph init',
|
|
150
|
-
'```',
|
|
151
|
-
'',
|
|
152
|
-
'Then run `pnpm install` at the project root.',
|
|
153
|
-
'',
|
|
154
|
-
].join('\n');
|
|
155
|
-
}
|
|
156
|
-
/** Create mode — empty target dir, write everything fresh. */
|
|
157
|
-
async function runCreate(options, _warn) {
|
|
158
|
-
const { targetDir, spec } = options;
|
|
159
|
-
if (!options.allowNonEmpty) {
|
|
160
|
-
const empty = await isDirEmptyEnough(targetDir);
|
|
161
|
-
if (!empty) {
|
|
162
|
-
throw new Error(`target directory ${targetDir} is not empty (pass allowNonEmpty or --force to override)`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const { planned, cliDir, appDir } = planFiles(spec, targetDir);
|
|
166
|
-
const hashes = {};
|
|
167
|
-
const files = [];
|
|
168
|
-
for (const p of planned) {
|
|
169
|
-
await writeFileEnsuringDir(p.absolutePath, p.content);
|
|
170
|
-
hashes[p.relativePath] = hashContent(p.content);
|
|
171
|
-
files.push({ absolutePath: p.absolutePath, relativePath: p.relativePath });
|
|
172
|
-
}
|
|
173
|
-
await writeProjectSpec(targetDir, spec);
|
|
174
|
-
files.push({
|
|
175
|
-
absolutePath: getSpecPath(targetDir),
|
|
176
|
-
relativePath: path.relative(targetDir, getSpecPath(targetDir)),
|
|
177
|
-
});
|
|
178
|
-
await writeHashes(targetDir, hashes);
|
|
179
|
-
return {
|
|
180
|
-
mode: 'create',
|
|
181
|
-
files,
|
|
182
|
-
skipped: [],
|
|
183
|
-
deleted: [],
|
|
184
|
-
migrated: false,
|
|
185
|
-
cliDir,
|
|
186
|
-
appDir,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
/** Update mode — reconcile against an existing project. */
|
|
190
|
-
async function runUpdate(options, warn, migrated) {
|
|
191
|
-
const { targetDir, spec } = options;
|
|
192
|
-
const force = !!options.force;
|
|
193
|
-
const { planned, cliDir, appDir } = planFiles(spec, targetDir);
|
|
194
|
-
const plannedByKey = new Map(planned.map((p) => [p.relativePath, p]));
|
|
195
|
-
const previous = await readHashes(targetDir);
|
|
196
|
-
const next = {};
|
|
197
|
-
const files = [];
|
|
198
|
-
const skipped = [];
|
|
199
|
-
const deleted = [];
|
|
200
|
-
// Step 1: write / patch / skip each planned file.
|
|
201
|
-
for (const p of planned) {
|
|
202
|
-
if (p.initOnly) {
|
|
203
|
-
// User-owned files (e.g. `src/framework.ts` with its `configSchema`)
|
|
204
|
-
// are emitted only on create. In update mode we never overwrite them
|
|
205
|
-
// and never re-create them if missing — the user may have deliberately
|
|
206
|
-
// deleted or inlined them. Keep whatever hash the previous run stored
|
|
207
|
-
// so the deletion pass (step 2) still considers them "ours".
|
|
208
|
-
const stored = previous[p.relativePath];
|
|
209
|
-
if (stored !== undefined)
|
|
210
|
-
next[p.relativePath] = stored;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
const existing = await hashFile(p.absolutePath);
|
|
214
|
-
if (existing === null) {
|
|
215
|
-
// Not yet on disk — fresh write.
|
|
216
|
-
await writeFileEnsuringDir(p.absolutePath, p.content);
|
|
217
|
-
next[p.relativePath] = hashContent(p.content);
|
|
218
|
-
files.push({
|
|
219
|
-
absolutePath: p.absolutePath,
|
|
220
|
-
relativePath: p.relativePath,
|
|
221
|
-
});
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
// File exists. Decide how to update it.
|
|
225
|
-
if (hasMarkers(p.content)) {
|
|
226
|
-
const existingContent = await fs.readFile(p.absolutePath, 'utf8');
|
|
227
|
-
const patched = hasMarkers(existingContent)
|
|
228
|
-
? spliceMarkerRegions(existingContent, p.content)
|
|
229
|
-
: p.content;
|
|
230
|
-
if (patched === existingContent) {
|
|
231
|
-
// No-op: regions already match.
|
|
232
|
-
next[p.relativePath] = hashContent(patched);
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
await writeFileEnsuringDir(p.absolutePath, patched);
|
|
236
|
-
next[p.relativePath] = hashContent(patched);
|
|
237
|
-
files.push({
|
|
238
|
-
absolutePath: p.absolutePath,
|
|
239
|
-
relativePath: p.relativePath,
|
|
240
|
-
});
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
// No markers — hash-protected overwrite.
|
|
244
|
-
const stored = previous[p.relativePath];
|
|
245
|
-
const pristine = stored !== undefined && stored === existing;
|
|
246
|
-
if (!pristine && !force) {
|
|
247
|
-
// User edited a managed file. Leave it alone.
|
|
248
|
-
warn(`skipped ${p.relativePath} — modified since last generation. Use --force to overwrite.`);
|
|
249
|
-
skipped.push({
|
|
250
|
-
absolutePath: p.absolutePath,
|
|
251
|
-
relativePath: p.relativePath,
|
|
252
|
-
});
|
|
253
|
-
// Preserve the previous hash so the file stays on our ledger.
|
|
254
|
-
if (stored !== undefined)
|
|
255
|
-
next[p.relativePath] = stored;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
if (existing === hashContent(p.content)) {
|
|
259
|
-
// Already up to date.
|
|
260
|
-
next[p.relativePath] = existing;
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
await writeFileEnsuringDir(p.absolutePath, p.content);
|
|
264
|
-
next[p.relativePath] = hashContent(p.content);
|
|
265
|
-
files.push({
|
|
266
|
-
absolutePath: p.absolutePath,
|
|
267
|
-
relativePath: p.relativePath,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
// Step 2: delete files that the new spec no longer emits, IF we still
|
|
271
|
-
// recognise their content (hash match). Otherwise warn and keep.
|
|
272
|
-
for (const [relPath, storedHash] of Object.entries(previous)) {
|
|
273
|
-
if (plannedByKey.has(relPath))
|
|
274
|
-
continue;
|
|
275
|
-
const abs = path.join(targetDir, relPath);
|
|
276
|
-
const onDisk = await hashFile(abs);
|
|
277
|
-
if (onDisk === null)
|
|
278
|
-
continue; // already gone
|
|
279
|
-
if (onDisk !== storedHash && !force) {
|
|
280
|
-
warn(`kept ${relPath} — modified since last generation; new spec no longer emits it.`);
|
|
281
|
-
next[relPath] = storedHash;
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
await fs.rm(abs, { force: true });
|
|
285
|
-
deleted.push({ absolutePath: abs, relativePath: relPath });
|
|
286
|
-
}
|
|
287
|
-
await writeProjectSpec(targetDir, spec);
|
|
288
|
-
await writeHashes(targetDir, next);
|
|
289
|
-
return {
|
|
290
|
-
mode: 'update',
|
|
291
|
-
files,
|
|
292
|
-
skipped,
|
|
293
|
-
deleted,
|
|
294
|
-
migrated,
|
|
295
|
-
cliDir,
|
|
296
|
-
appDir,
|
|
297
|
-
};
|
|
298
11
|
}
|