@powerhousedao/ph-clint-cli 0.1.0-dev.34 → 0.1.0-dev.36
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/codegen/actions.d.ts +1 -1
- package/dist/codegen/actions.js +38 -8
- package/dist/codegen/generated.d.ts +17 -0
- package/dist/codegen/generated.js +49 -0
- package/dist/codegen/index.js +105 -2
- package/dist/codegen/scaffold.d.ts +10 -0
- package/dist/codegen/scaffold.js +32 -8
- package/dist/commands/clint-project-init.js +4 -11
- package/dist/commands/clint-project-regen.js +19 -1
- package/dist/spec/ensure-document.d.ts +17 -0
- package/dist/spec/ensure-document.js +62 -0
- package/dist/triggers/spec-change.js +20 -16
- package/package.json +4 -4
|
@@ -38,5 +38,5 @@ export interface PostGenActionResult {
|
|
|
38
38
|
reason?: string;
|
|
39
39
|
durationMs: number;
|
|
40
40
|
}
|
|
41
|
-
export declare function collectPostGenActions(result: GenerateProjectResult, spec: ClintProjectSpec): PostGenAction[]
|
|
41
|
+
export declare function collectPostGenActions(result: GenerateProjectResult, spec: ClintProjectSpec): Promise<PostGenAction[]>;
|
|
42
42
|
export declare function runPostGenActions(actions: PostGenAction[], ctx: PostGenActionContext): Promise<PostGenActionResult[]>;
|
package/dist/codegen/actions.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* `runPostGenActions()` executes them sequentially with proper ordering,
|
|
8
8
|
* failure cascading, and log output.
|
|
9
9
|
*/
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
10
11
|
import path from 'node:path';
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Fixed execution order — index defines precedence
|
|
@@ -45,9 +46,21 @@ function dependsOn(downstream, upstream) {
|
|
|
45
46
|
return di > ui;
|
|
46
47
|
}
|
|
47
48
|
// ---------------------------------------------------------------------------
|
|
48
|
-
//
|
|
49
|
+
// Helpers
|
|
49
50
|
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
+
async function fileExists(filePath) {
|
|
52
|
+
try {
|
|
53
|
+
await fs.access(filePath);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// collectPostGenActions
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
export async function collectPostGenActions(result, spec) {
|
|
51
64
|
const split = result.appDir !== null;
|
|
52
65
|
const relPaths = new Set(result.files.map((f) => f.relativePath));
|
|
53
66
|
// Determine the earliest triggered action in the chain
|
|
@@ -55,13 +68,21 @@ export function collectPostGenActions(result, spec) {
|
|
|
55
68
|
if (split) {
|
|
56
69
|
const appFolder = result.appDir ? path.basename(result.appDir) : '';
|
|
57
70
|
const cliFolder = path.basename(result.cliDir);
|
|
58
|
-
// ph-init needed: app dir
|
|
71
|
+
// ph-init needed: app dir placeholder written but app not yet initialized.
|
|
72
|
+
// Check the actual filesystem — if package.json already exists on disk,
|
|
73
|
+
// ph init has already run and must not run again.
|
|
59
74
|
const appPkgJsonChanged = relPaths.has(path.join(appFolder, 'package.json'));
|
|
60
|
-
// Check for the .gitkeep — signals a new app dir that needs ph-init
|
|
61
75
|
const appGitkeepWritten = relPaths.has(path.join(appFolder, '.gitkeep'));
|
|
76
|
+
// ph-init needed: app dir placeholder written but app not yet initialized.
|
|
77
|
+
// Check the actual filesystem — if package.json already exists on disk,
|
|
78
|
+
// ph init has already run and must not run again.
|
|
62
79
|
if (appGitkeepWritten && !appPkgJsonChanged) {
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
const appAlreadyInitialized = result.appDir
|
|
81
|
+
? await fileExists(path.join(result.appDir, 'package.json'))
|
|
82
|
+
: false;
|
|
83
|
+
if (!appAlreadyInitialized) {
|
|
84
|
+
earliest = 'ph-init';
|
|
85
|
+
}
|
|
65
86
|
}
|
|
66
87
|
// Migration always triggers full rebuild
|
|
67
88
|
if (result.migrated) {
|
|
@@ -87,11 +108,11 @@ export function collectPostGenActions(result, spec) {
|
|
|
87
108
|
earliest = 'cli-install';
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
|
-
// CLI source regenerated (.ts files)
|
|
111
|
+
// CLI source or prompts regenerated (.ts files, agent profile .md files)
|
|
91
112
|
if (!earliest) {
|
|
92
113
|
for (const rel of relPaths) {
|
|
93
114
|
if (rel.startsWith(cliFolder + '/') &&
|
|
94
|
-
rel.endsWith('.ts')) {
|
|
115
|
+
(rel.endsWith('.ts') || rel.endsWith('.md'))) {
|
|
95
116
|
earliest = 'cli-build';
|
|
96
117
|
break;
|
|
97
118
|
}
|
|
@@ -285,6 +306,15 @@ async function executeAction(action, ctx) {
|
|
|
285
306
|
ctx.log(` ${result.reason ?? 'ph init did not run'}`);
|
|
286
307
|
return false;
|
|
287
308
|
}
|
|
309
|
+
if (result.exitCode === 0) {
|
|
310
|
+
// Mark app as initialized in generated state
|
|
311
|
+
const { readGeneratedState, writeGeneratedState } = await import('./generated.js');
|
|
312
|
+
const gen = await readGeneratedState(action.targetDir);
|
|
313
|
+
if (gen) {
|
|
314
|
+
gen.appInitialized = true;
|
|
315
|
+
await writeGeneratedState(action.targetDir, gen);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
288
318
|
return result.exitCode === 0;
|
|
289
319
|
}
|
|
290
320
|
case 'app-install':
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ClintProjectSpec } from '../spec/types.js';
|
|
2
|
+
export interface GeneratedState {
|
|
3
|
+
name: string;
|
|
4
|
+
scope: string | undefined;
|
|
5
|
+
cliFolderName: string;
|
|
6
|
+
appFolderName: string;
|
|
7
|
+
/** True once `ph init` has run successfully in the app dir. */
|
|
8
|
+
appInitialized: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function getGeneratedPath(targetDir: string): string;
|
|
11
|
+
export declare function readGeneratedState(targetDir: string): Promise<GeneratedState | null>;
|
|
12
|
+
export declare function writeGeneratedState(targetDir: string, state: GeneratedState): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Derive a `GeneratedState` from a spec. The `appInitialized` flag must be
|
|
15
|
+
* supplied by the caller (it depends on filesystem state, not the spec).
|
|
16
|
+
*/
|
|
17
|
+
export declare function generatedStateFromSpec(spec: ClintProjectSpec, appInitialized: boolean): GeneratedState;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks the derived state from the last successful code generation.
|
|
3
|
+
*
|
|
4
|
+
* Stored at `{targetDir}/.ph/ph-clint-cli/generated.json`. Used to detect
|
|
5
|
+
* folder renames (name/scope changes) and re-apply patches that depend on
|
|
6
|
+
* the project identity.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { getAppFolderName, getCliFolderName, } from '../spec/types.js';
|
|
11
|
+
const GEN_DIR = path.join('.ph', 'ph-clint-cli');
|
|
12
|
+
const GEN_FILE = 'generated.json';
|
|
13
|
+
export function getGeneratedPath(targetDir) {
|
|
14
|
+
return path.join(targetDir, GEN_DIR, GEN_FILE);
|
|
15
|
+
}
|
|
16
|
+
export async function readGeneratedState(targetDir) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await fs.readFile(getGeneratedPath(targetDir), 'utf8');
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
if (err &&
|
|
23
|
+
typeof err === 'object' &&
|
|
24
|
+
'code' in err &&
|
|
25
|
+
err.code === 'ENOENT') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function writeGeneratedState(targetDir, state) {
|
|
32
|
+
const file = getGeneratedPath(targetDir);
|
|
33
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
34
|
+
await fs.writeFile(file, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Derive a `GeneratedState` from a spec. The `appInitialized` flag must be
|
|
38
|
+
* supplied by the caller (it depends on filesystem state, not the spec).
|
|
39
|
+
*/
|
|
40
|
+
export function generatedStateFromSpec(spec, appInitialized) {
|
|
41
|
+
const split = spec.features.powerhouse !== 'Disabled';
|
|
42
|
+
return {
|
|
43
|
+
name: spec.name,
|
|
44
|
+
scope: spec.scope,
|
|
45
|
+
cliFolderName: split ? getCliFolderName(spec) : '',
|
|
46
|
+
appFolderName: split ? getAppFolderName(spec) : '',
|
|
47
|
+
appInitialized,
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/codegen/index.js
CHANGED
|
@@ -37,6 +37,8 @@ import { writeFileEnsuringDir, isDirEmptyEnough } from './write.js';
|
|
|
37
37
|
import { migrateFlatToSplit } from './migrate/flat-to-split.js';
|
|
38
38
|
import { CLI_FILE_BUILDERS, getProfileFileBuilders, buildReadme, buildRootPackageJson, buildPublishConfigJs, buildAppIndexTs, } from './builders/index.js';
|
|
39
39
|
import { collectPostGenActions, } from './actions.js';
|
|
40
|
+
import { readGeneratedState, writeGeneratedState, generatedStateFromSpec, } from './generated.js';
|
|
41
|
+
import { patchAppPackageName } from './scaffold.js';
|
|
40
42
|
/**
|
|
41
43
|
* Generate / reconcile a project at `targetDir` from the given spec.
|
|
42
44
|
*/
|
|
@@ -156,6 +158,82 @@ function buildAppReadmeContent(spec) {
|
|
|
156
158
|
'',
|
|
157
159
|
].join('\n');
|
|
158
160
|
}
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Folder rename handling
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
async function fileExists(filePath) {
|
|
165
|
+
try {
|
|
166
|
+
await fs.access(filePath);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Detect and execute folder renames when the project name changes.
|
|
175
|
+
*
|
|
176
|
+
* Reads the previous `GeneratedState` to find the old folder names, compares
|
|
177
|
+
* with the new spec's folder names. If they differ:
|
|
178
|
+
* 1. Renames the directories on disk.
|
|
179
|
+
* 2. Rewrites hash-record keys from old → new prefixes.
|
|
180
|
+
*
|
|
181
|
+
* Must run BEFORE `planFiles()` so the reconciliation loop sees the files
|
|
182
|
+
* in their new locations.
|
|
183
|
+
*/
|
|
184
|
+
async function handleFolderRename(targetDir, spec, prev, warn) {
|
|
185
|
+
const split = spec.features.powerhouse !== 'Disabled';
|
|
186
|
+
if (!split || !prev || !prev.cliFolderName) {
|
|
187
|
+
return { renamed: false, appInitialized: prev?.appInitialized ?? false };
|
|
188
|
+
}
|
|
189
|
+
const newCliFolder = getCliFolderName(spec);
|
|
190
|
+
const newAppFolder = getAppFolderName(spec);
|
|
191
|
+
const oldCliFolder = prev.cliFolderName;
|
|
192
|
+
const oldAppFolder = prev.appFolderName;
|
|
193
|
+
if (oldCliFolder === newCliFolder && oldAppFolder === newAppFolder) {
|
|
194
|
+
return { renamed: false, appInitialized: prev.appInitialized };
|
|
195
|
+
}
|
|
196
|
+
// Rename CLI folder
|
|
197
|
+
if (oldCliFolder !== newCliFolder) {
|
|
198
|
+
const oldPath = path.join(targetDir, oldCliFolder);
|
|
199
|
+
const newPath = path.join(targetDir, newCliFolder);
|
|
200
|
+
if (await fileExists(oldPath)) {
|
|
201
|
+
if (await fileExists(newPath)) {
|
|
202
|
+
warn(`cannot rename ${oldCliFolder} → ${newCliFolder}: target already exists`);
|
|
203
|
+
return { renamed: false, appInitialized: prev.appInitialized };
|
|
204
|
+
}
|
|
205
|
+
await fs.rename(oldPath, newPath);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Rename app folder
|
|
209
|
+
if (oldAppFolder !== newAppFolder) {
|
|
210
|
+
const oldPath = path.join(targetDir, oldAppFolder);
|
|
211
|
+
const newPath = path.join(targetDir, newAppFolder);
|
|
212
|
+
if (await fileExists(oldPath)) {
|
|
213
|
+
if (await fileExists(newPath)) {
|
|
214
|
+
warn(`cannot rename ${oldAppFolder} → ${newAppFolder}: target already exists`);
|
|
215
|
+
return { renamed: false, appInitialized: prev.appInitialized };
|
|
216
|
+
}
|
|
217
|
+
await fs.rename(oldPath, newPath);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Rewrite hash keys from old folder prefixes to new ones
|
|
221
|
+
const hashes = await readHashes(targetDir);
|
|
222
|
+
const remapped = {};
|
|
223
|
+
for (const [key, value] of Object.entries(hashes)) {
|
|
224
|
+
let newKey = key;
|
|
225
|
+
if (oldCliFolder !== newCliFolder && key.startsWith(oldCliFolder + '/')) {
|
|
226
|
+
newKey = newCliFolder + key.slice(oldCliFolder.length);
|
|
227
|
+
}
|
|
228
|
+
else if (oldAppFolder !== newAppFolder &&
|
|
229
|
+
key.startsWith(oldAppFolder + '/')) {
|
|
230
|
+
newKey = newAppFolder + key.slice(oldAppFolder.length);
|
|
231
|
+
}
|
|
232
|
+
remapped[newKey] = value;
|
|
233
|
+
}
|
|
234
|
+
await writeHashes(targetDir, remapped);
|
|
235
|
+
return { renamed: true, appInitialized: prev.appInitialized };
|
|
236
|
+
}
|
|
159
237
|
/** Create mode — empty target dir, write everything fresh. */
|
|
160
238
|
async function runCreate(options, _warn) {
|
|
161
239
|
const { targetDir, spec } = options;
|
|
@@ -179,6 +257,8 @@ async function runCreate(options, _warn) {
|
|
|
179
257
|
relativePath: path.relative(targetDir, getSpecPath(targetDir)),
|
|
180
258
|
});
|
|
181
259
|
await writeHashes(targetDir, hashes);
|
|
260
|
+
// App is not yet initialized on create — ph-init runs as a post-gen action.
|
|
261
|
+
await writeGeneratedState(targetDir, generatedStateFromSpec(spec, false));
|
|
182
262
|
const result = {
|
|
183
263
|
mode: 'create',
|
|
184
264
|
files,
|
|
@@ -189,13 +269,16 @@ async function runCreate(options, _warn) {
|
|
|
189
269
|
appDir,
|
|
190
270
|
pendingActions: [],
|
|
191
271
|
};
|
|
192
|
-
result.pendingActions = collectPostGenActions(result, spec);
|
|
272
|
+
result.pendingActions = await collectPostGenActions(result, spec);
|
|
193
273
|
return result;
|
|
194
274
|
}
|
|
195
275
|
/** Update mode — reconcile against an existing project. */
|
|
196
276
|
async function runUpdate(options, warn, migrated) {
|
|
197
277
|
const { targetDir, spec } = options;
|
|
198
278
|
const force = !!options.force;
|
|
279
|
+
// --- Folder rename detection ---
|
|
280
|
+
const prev = await readGeneratedState(targetDir);
|
|
281
|
+
const { renamed, appInitialized: appWasInitialized } = await handleFolderRename(targetDir, spec, prev, warn);
|
|
199
282
|
const { planned, cliDir, appDir } = planFiles(spec, targetDir);
|
|
200
283
|
const plannedByKey = new Map(planned.map((p) => [p.relativePath, p]));
|
|
201
284
|
const previous = await readHashes(targetDir);
|
|
@@ -299,6 +382,26 @@ async function runUpdate(options, warn, migrated) {
|
|
|
299
382
|
}
|
|
300
383
|
await writeProjectSpec(targetDir, spec);
|
|
301
384
|
await writeHashes(targetDir, next);
|
|
385
|
+
// Re-patch app package.json if name or scope changed and app is initialized.
|
|
386
|
+
// Add to `files` so collectPostGenActions sees the change and triggers
|
|
387
|
+
// app-install → app-build → cli-install → cli-build.
|
|
388
|
+
const appIsInitialized = appDir
|
|
389
|
+
? await fileExists(path.join(appDir, 'package.json'))
|
|
390
|
+
: false;
|
|
391
|
+
const nameOrScopeChanged = prev
|
|
392
|
+
? prev.name !== spec.name || prev.scope !== spec.scope
|
|
393
|
+
: false;
|
|
394
|
+
if (appDir && appIsInitialized && nameOrScopeChanged) {
|
|
395
|
+
const patched = await patchAppPackageName(appDir, spec, warn);
|
|
396
|
+
if (patched) {
|
|
397
|
+
const appFolder = path.basename(appDir);
|
|
398
|
+
files.push({
|
|
399
|
+
absolutePath: path.join(appDir, 'package.json'),
|
|
400
|
+
relativePath: path.join(appFolder, 'package.json'),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
await writeGeneratedState(targetDir, generatedStateFromSpec(spec, appIsInitialized));
|
|
302
405
|
const result = {
|
|
303
406
|
mode: 'update',
|
|
304
407
|
files,
|
|
@@ -309,6 +412,6 @@ async function runUpdate(options, warn, migrated) {
|
|
|
309
412
|
appDir,
|
|
310
413
|
pendingActions: [],
|
|
311
414
|
};
|
|
312
|
-
result.pendingActions = collectPostGenActions(result, spec);
|
|
415
|
+
result.pendingActions = await collectPostGenActions(result, spec);
|
|
313
416
|
return result;
|
|
314
417
|
}
|
|
@@ -34,6 +34,16 @@ export interface PhInitResult {
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function getPhVersion(binName: string): string | undefined;
|
|
36
36
|
export declare function runPhInit(options: PhInitOptions): Promise<PhInitResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Patch the app `package.json` name to the scoped form (`@scope/name-app`).
|
|
39
|
+
* Called after `ph init` and after name/scope renames.
|
|
40
|
+
*
|
|
41
|
+
* Returns `true` if the file was actually changed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function patchAppPackageName(appDir: string, spec: {
|
|
44
|
+
name: string;
|
|
45
|
+
scope?: string;
|
|
46
|
+
}, log?: (msg: string) => void): Promise<boolean>;
|
|
37
47
|
export interface PnpmInstallOptions {
|
|
38
48
|
/** Directories to run `pnpm install` in, in order. */
|
|
39
49
|
dirs: string[];
|
package/dist/codegen/scaffold.js
CHANGED
|
@@ -31,6 +31,15 @@ export async function runPhInit(options) {
|
|
|
31
31
|
const log = options.log ?? (() => { });
|
|
32
32
|
const binName = options.binName ?? 'ph';
|
|
33
33
|
const appFolder = path.basename(options.appDir);
|
|
34
|
+
// Guard: never re-init an already-initialized app directory.
|
|
35
|
+
try {
|
|
36
|
+
await fs.access(path.join(options.appDir, 'package.json'));
|
|
37
|
+
log(`App directory ${appFolder} already initialized — skipping ph init.`);
|
|
38
|
+
return { ran: false, reason: 'already-initialized' };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// No package.json — proceed with init.
|
|
42
|
+
}
|
|
34
43
|
if (!(await hasCommandOnPath(binName))) {
|
|
35
44
|
log(`Skipping \`${binName} init\` — \`${binName}\` is not on PATH. ` +
|
|
36
45
|
`Install @powerhousedao/ph-cli and re-run manually inside ${appFolder}/.`);
|
|
@@ -76,25 +85,40 @@ export async function runPhInit(options) {
|
|
|
76
85
|
await patchScopedName(options, appFolder, log);
|
|
77
86
|
return { ran: true, exitCode: result.exitCode };
|
|
78
87
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Patch the app `package.json` name to the scoped form (`@scope/name-app`).
|
|
90
|
+
* Called after `ph init` and after name/scope renames.
|
|
91
|
+
*
|
|
92
|
+
* Returns `true` if the file was actually changed.
|
|
93
|
+
*/
|
|
94
|
+
export async function patchAppPackageName(appDir, spec, log) {
|
|
95
|
+
const appFolder = path.basename(appDir);
|
|
96
|
+
const scopedName = spec.scope
|
|
97
|
+
? `@${spec.scope}/${appFolder}`
|
|
98
|
+
: appFolder;
|
|
99
|
+
const appPkgJsonPath = path.join(appDir, 'package.json');
|
|
83
100
|
try {
|
|
84
101
|
const raw = await fs.readFile(appPkgJsonPath, 'utf8');
|
|
85
102
|
const pkg = JSON.parse(raw);
|
|
86
|
-
const scopedName = `@${options.spec.scope}/${appFolder}`;
|
|
87
103
|
if (pkg.name !== scopedName) {
|
|
88
104
|
pkg.name = scopedName;
|
|
89
|
-
|
|
105
|
+
if (spec.scope) {
|
|
106
|
+
pkg.publishConfig = { access: 'public' };
|
|
107
|
+
}
|
|
90
108
|
await fs.writeFile(appPkgJsonPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
91
|
-
log(`Patched app package name to ${scopedName}`);
|
|
109
|
+
log?.(`Patched app package name to ${scopedName}`);
|
|
110
|
+
return true;
|
|
92
111
|
}
|
|
112
|
+
return false;
|
|
93
113
|
}
|
|
94
114
|
catch (err) {
|
|
95
|
-
log(`Warning: could not patch app package name: ${err instanceof Error ? err.message : String(err)}`);
|
|
115
|
+
log?.(`Warning: could not patch app package name: ${err instanceof Error ? err.message : String(err)}`);
|
|
116
|
+
return false;
|
|
96
117
|
}
|
|
97
118
|
}
|
|
119
|
+
async function patchScopedName(options, _appFolder, log) {
|
|
120
|
+
await patchAppPackageName(options.appDir, options.spec, log);
|
|
121
|
+
}
|
|
98
122
|
export async function runPnpmInstall(options) {
|
|
99
123
|
const log = options.log ?? (() => { });
|
|
100
124
|
const binName = options.binName ?? 'pnpm';
|
|
@@ -13,14 +13,12 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { defineCommand } from '@powerhousedao/ph-clint';
|
|
16
|
-
import { importSpec } from '@powerhousedao/ph-clint-app/document-models/ph-clint-project';
|
|
17
16
|
import { z } from 'zod';
|
|
18
17
|
import { clintProjectSpecSchema, } from '../spec/types.js';
|
|
19
18
|
import { generateProject } from '../codegen/index.js';
|
|
20
19
|
import { isDirEmptyEnough } from '../codegen/write.js';
|
|
21
20
|
import { runPostGenActions } from '../codegen/actions.js';
|
|
22
|
-
import {
|
|
23
|
-
import { specToImportInput } from '../triggers/spec-change.js';
|
|
21
|
+
import { ensureSpecDocument } from '../spec/ensure-document.js';
|
|
24
22
|
const inputSchema = z.object({
|
|
25
23
|
dir: z
|
|
26
24
|
.string()
|
|
@@ -127,14 +125,9 @@ export const init = defineCommand({
|
|
|
127
125
|
try {
|
|
128
126
|
const reactor = await getReactor();
|
|
129
127
|
if (reactor) {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const importInput = specToImportInput(spec);
|
|
134
|
-
await reactor.client.execute(docId, 'main', [importSpec(importInput)]);
|
|
135
|
-
spec.documentId = docId;
|
|
136
|
-
spec.documentType = 'powerhouse/ph-clint-project';
|
|
137
|
-
await writeProjectSpec(targetDir, spec);
|
|
128
|
+
const { docId } = await ensureSpecDocument({
|
|
129
|
+
spec, targetDir, reactor, folders,
|
|
130
|
+
});
|
|
138
131
|
stdout(`Created spec document ${docId} in Clint Folders/specs/${spec.name}\n`);
|
|
139
132
|
}
|
|
140
133
|
}
|
|
@@ -12,6 +12,7 @@ import { z } from 'zod';
|
|
|
12
12
|
import { readProjectSpec } from '../spec/file.js';
|
|
13
13
|
import { generateProject } from '../codegen/index.js';
|
|
14
14
|
import { runPostGenActions } from '../codegen/actions.js';
|
|
15
|
+
import { ensureSpecDocument } from '../spec/ensure-document.js';
|
|
15
16
|
const inputSchema = z.object({
|
|
16
17
|
dir: z
|
|
17
18
|
.string()
|
|
@@ -26,7 +27,7 @@ export const regen = defineCommand({
|
|
|
26
27
|
id: 'clint-project-regen',
|
|
27
28
|
description: 'Regenerate project files from the persisted spec',
|
|
28
29
|
inputSchema,
|
|
29
|
-
execute: async (input, { workdir, stdout, log, runProcess }) => {
|
|
30
|
+
execute: async (input, { workdir, stdout, log, runProcess, reactor: getReactor, folders }) => {
|
|
30
31
|
const targetDir = path.resolve(workdir, input.dir ?? '.');
|
|
31
32
|
const spec = await readProjectSpec(targetDir);
|
|
32
33
|
if (!spec) {
|
|
@@ -53,6 +54,23 @@ export const regen = defineCommand({
|
|
|
53
54
|
else
|
|
54
55
|
stdout('warning: ' + w + '\n');
|
|
55
56
|
}
|
|
57
|
+
// Ensure spec document exists in drive (recover if deleted)
|
|
58
|
+
if (getReactor && folders) {
|
|
59
|
+
try {
|
|
60
|
+
const reactor = await getReactor();
|
|
61
|
+
if (reactor) {
|
|
62
|
+
const { docId, created } = await ensureSpecDocument({
|
|
63
|
+
spec, targetDir, reactor, folders,
|
|
64
|
+
});
|
|
65
|
+
if (created) {
|
|
66
|
+
lines.push(`Created spec document ${docId} in Clint Folders/specs/${spec.name}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
log?.warn(`Could not ensure spec document: ${err instanceof Error ? err.message : String(err)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
56
74
|
stdout(lines.join('\n') + '\n');
|
|
57
75
|
// Run post-generation actions (install, build, skills-sync).
|
|
58
76
|
await runPostGenActions(result.pendingActions, {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FolderOperations } from '@powerhousedao/ph-clint';
|
|
2
|
+
import type { ReactorContext, DocumentRegistry } from '@powerhousedao/ph-clint';
|
|
3
|
+
import type { ClintProjectSpec } from './types.js';
|
|
4
|
+
export interface EnsureSpecDocumentResult {
|
|
5
|
+
docId: string;
|
|
6
|
+
created: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Ensure the spec document exists in the personal drive. Creates one if
|
|
10
|
+
* missing or if the existing `documentId` no longer resolves to a drive node.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ensureSpecDocument<R extends DocumentRegistry = DocumentRegistry>(opts: {
|
|
13
|
+
spec: ClintProjectSpec;
|
|
14
|
+
targetDir: string;
|
|
15
|
+
reactor: ReactorContext<R>;
|
|
16
|
+
folders: FolderOperations;
|
|
17
|
+
}): Promise<EnsureSpecDocumentResult>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure a ph-clint-project spec document exists in the personal drive.
|
|
3
|
+
*
|
|
4
|
+
* Single code path used by:
|
|
5
|
+
* - `clint-project-init` (after scaffolding)
|
|
6
|
+
* - `clint-project-regen` (recover deleted docs)
|
|
7
|
+
* - `spec-change` trigger (auto-link unlinked projects)
|
|
8
|
+
*
|
|
9
|
+
* If the spec already has a `documentId` that exists in the drive, this is
|
|
10
|
+
* a no-op. If the document was deleted (or never created), a new one is
|
|
11
|
+
* created, added to the drive at `specs/{name}`, and the spec JSON on disk
|
|
12
|
+
* is updated with the new `documentId`.
|
|
13
|
+
*/
|
|
14
|
+
import { importSpec } from '@powerhousedao/ph-clint-app/document-models/ph-clint-project';
|
|
15
|
+
import { writeProjectSpec } from './file.js';
|
|
16
|
+
import { specToImportInput } from '../triggers/spec-change.js';
|
|
17
|
+
const DOCUMENT_TYPE = 'powerhouse/ph-clint-project';
|
|
18
|
+
/**
|
|
19
|
+
* Ensure the spec document exists in the personal drive. Creates one if
|
|
20
|
+
* missing or if the existing `documentId` no longer resolves to a drive node.
|
|
21
|
+
*/
|
|
22
|
+
export async function ensureSpecDocument(opts) {
|
|
23
|
+
const { spec, targetDir, reactor, folders } = opts;
|
|
24
|
+
// Check if the document already exists in the drive's folder tree
|
|
25
|
+
if (spec.documentId) {
|
|
26
|
+
const entries = await folders.findByType(DOCUMENT_TYPE);
|
|
27
|
+
const inDrive = entries.some((e) => e.id === spec.documentId);
|
|
28
|
+
if (inDrive)
|
|
29
|
+
return { docId: spec.documentId, created: false };
|
|
30
|
+
}
|
|
31
|
+
// Reuse existing document if it's still in the reactor, otherwise create new
|
|
32
|
+
let docId;
|
|
33
|
+
if (spec.documentId) {
|
|
34
|
+
try {
|
|
35
|
+
const existing = await reactor.client.get(spec.documentId);
|
|
36
|
+
if (existing) {
|
|
37
|
+
docId = spec.documentId;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const newDoc = await reactor.client.createEmpty(DOCUMENT_TYPE);
|
|
41
|
+
docId = newDoc.header.id;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
const newDoc = await reactor.client.createEmpty(DOCUMENT_TYPE);
|
|
46
|
+
docId = newDoc.header.id;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const newDoc = await reactor.client.createEmpty(DOCUMENT_TYPE);
|
|
51
|
+
docId = newDoc.header.id;
|
|
52
|
+
}
|
|
53
|
+
// Add to the drive's folder tree
|
|
54
|
+
await folders.addDocument(docId, `specs/${spec.name}`, spec.name);
|
|
55
|
+
const importInput = specToImportInput(spec);
|
|
56
|
+
await reactor.client.execute(docId, 'main', [importSpec(importInput)]);
|
|
57
|
+
// Persist the link back to disk
|
|
58
|
+
spec.documentId = docId;
|
|
59
|
+
spec.documentType = DOCUMENT_TYPE;
|
|
60
|
+
await writeProjectSpec(targetDir, spec);
|
|
61
|
+
return { docId, created: true };
|
|
62
|
+
}
|
|
@@ -17,12 +17,12 @@ import crypto from 'node:crypto';
|
|
|
17
17
|
import fs from 'node:fs/promises';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { scanProjects, getProjectMapping } from '@powerhousedao/ph-clint';
|
|
20
|
-
import { importSpec } from '@powerhousedao/ph-clint-app/document-models/ph-clint-project';
|
|
21
20
|
import { createDocumentChangeTrigger } from '../framework.js';
|
|
22
21
|
import { generateProject } from '../codegen/index.js';
|
|
23
22
|
import { runPostGenActions } from '../codegen/actions.js';
|
|
24
23
|
import { specFromDocumentState } from '../spec/from-document.js';
|
|
25
24
|
import { readProjectSpec, writeProjectSpec } from '../spec/file.js';
|
|
25
|
+
import { ensureSpecDocument } from '../spec/ensure-document.js';
|
|
26
26
|
import { clintProject } from '../services/clint-project.js';
|
|
27
27
|
const DOCUMENT_TYPE = 'powerhouse/ph-clint-project';
|
|
28
28
|
const HASH_DIR = path.join('.ph', 'ph-clint-cli');
|
|
@@ -121,24 +121,28 @@ export const specChangeTrigger = createDocumentChangeTrigger({
|
|
|
121
121
|
? scanProjects(ctx.context.workdir, scanner)
|
|
122
122
|
: [];
|
|
123
123
|
const mapping = await getProjectMapping(scanResults, folders);
|
|
124
|
-
// Auto-create spec documents for unlinked projects
|
|
124
|
+
// Auto-create spec documents for unlinked or orphaned projects
|
|
125
125
|
const reactor = await ctx.reactor();
|
|
126
126
|
if (reactor && folders) {
|
|
127
127
|
for (const entry of mapping) {
|
|
128
|
-
if (entry.path
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const docId =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
128
|
+
if (!entry.path)
|
|
129
|
+
continue;
|
|
130
|
+
const existingSpec = await readProjectSpec(entry.path);
|
|
131
|
+
if (!existingSpec)
|
|
132
|
+
continue;
|
|
133
|
+
try {
|
|
134
|
+
const { docId, created } = await ensureSpecDocument({
|
|
135
|
+
spec: existingSpec,
|
|
136
|
+
targetDir: entry.path,
|
|
137
|
+
reactor,
|
|
138
|
+
folders,
|
|
139
|
+
});
|
|
140
|
+
if (created) {
|
|
141
|
+
log?.info(`${TAG} linked project ${entry.name} to document ${docId}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
log?.warn(`${TAG} could not ensure spec document for ${entry.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powerhousedao/ph-clint-cli",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.36",
|
|
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.
|
|
33
|
-
"@powerhousedao/ph-clint-app": "^0.1.0-dev.
|
|
34
|
-
"@powerhousedao/ph-clint-dev": "^0.1.0-dev.
|
|
32
|
+
"@powerhousedao/ph-clint": "^0.1.0-dev.36",
|
|
33
|
+
"@powerhousedao/ph-clint-app": "^0.1.0-dev.36",
|
|
34
|
+
"@powerhousedao/ph-clint-dev": "^0.1.0-dev.36",
|
|
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",
|