@powerhousedao/ph-clint-cli 0.1.0-dev.52 → 0.1.0-dev.54
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 +3 -2
- package/dist/codegen/actions.d.ts +1 -46
- package/dist/codegen/actions.js +1 -424
- package/dist/codegen/builders/app-index-ts.js +24 -20
- package/dist/codegen/builders/cli-package-json.d.ts +1 -8
- package/dist/codegen/builders/cli-package-json.js +1 -128
- package/dist/codegen/builders/index.d.ts +1 -51
- package/dist/codegen/builders/index.js +1 -84
- package/dist/codegen/exec.d.ts +1 -2
- package/dist/codegen/exec.js +1 -14
- package/dist/codegen/generated.d.ts +1 -17
- package/dist/codegen/generated.js +1 -49
- package/dist/codegen/hashes.d.ts +1 -6
- package/dist/codegen/hashes.js +1 -60
- package/dist/codegen/index.d.ts +8 -39
- package/dist/codegen/index.js +6 -407
- package/dist/codegen/markers.d.ts +1 -36
- package/dist/codegen/markers.js +1 -86
- package/dist/codegen/scaffold.d.ts +1 -72
- package/dist/codegen/scaffold.js +1 -150
- package/dist/codegen/write.d.ts +1 -6
- package/dist/codegen/write.js +1 -32
- package/dist/commands/clint-project-init.js +2 -2
- package/dist/commands/clint-project-regen.js +1 -1
- package/dist/commands/clint-skills-sync.d.ts +1 -1
- package/dist/commands/clint-skills-sync.js +1 -1
- package/dist/framework.js +1 -1
- package/dist/skills/sync.d.ts +1 -40
- package/dist/skills/sync.js +1 -119
- package/dist/spec/ensure-document.js +9 -15
- package/dist/spec/file.d.ts +1 -4
- package/dist/spec/file.js +1 -34
- package/dist/spec/types.d.ts +1 -164
- package/dist/spec/types.js +1 -209
- package/dist/triggers/publish-trigger.js +1 -1
- package/dist/triggers/spec-change.js +7 -1
- package/package.json +9 -9
package/dist/codegen/index.js
CHANGED
|
@@ -1,412 +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, getProfileFileBuilders, buildReadme, buildRootPackageJson, buildPublishConfigJs, } from './builders/index.js';
|
|
39
|
-
import { collectPostGenActions, } from './actions.js';
|
|
40
|
-
import { readGeneratedState, writeGeneratedState, generatedStateFromSpec, } from './generated.js';
|
|
41
|
-
import { patchAppPackageName } from './scaffold.js';
|
|
42
|
-
/**
|
|
43
|
-
* Generate / reconcile a project at `targetDir` from the given spec.
|
|
2
|
+
* Re-exports from @powerhousedao/ph-clint-dev/codegen with CLI_VERSION default.
|
|
44
3
|
*/
|
|
4
|
+
import { generateProject as _generateProject, } from '@powerhousedao/ph-clint-dev/codegen';
|
|
5
|
+
import { CLI_VERSION } from '../config.js';
|
|
45
6
|
export async function generateProject(options) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const resolvedMode = resolveMode(options.mode ?? 'auto', existingSpec !== null);
|
|
50
|
-
// Flat → split migration, if needed. Runs BEFORE we compute planned files
|
|
51
|
-
// so the hash record and on-disk state are already in split-layout shape
|
|
52
|
-
// by the time the normal reconciliation runs.
|
|
53
|
-
//
|
|
54
|
-
// Use GeneratedState (what was *actually generated* last time) rather than
|
|
55
|
-
// the on-disk spec to detect the layout flip — when the user edits the spec
|
|
56
|
-
// file before running regen, both existingSpec and spec already say "Reactor"
|
|
57
|
-
// and the old comparison would never trigger.
|
|
58
|
-
let migrated = false;
|
|
59
|
-
if (resolvedMode === 'update' && spec.features.powerhouse !== 'Disabled') {
|
|
60
|
-
const prev = await readGeneratedState(targetDir);
|
|
61
|
-
const wasFlatLayout = prev ? prev.cliFolderName === '' : existingSpec?.features.powerhouse === 'Disabled';
|
|
62
|
-
if (wasFlatLayout) {
|
|
63
|
-
await migrateFlatToSplit({
|
|
64
|
-
targetDir,
|
|
65
|
-
spec,
|
|
66
|
-
force: options.force,
|
|
67
|
-
onWarn: warn,
|
|
68
|
-
});
|
|
69
|
-
migrated = true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
// Dispatch to the mode-specific flow.
|
|
73
|
-
if (resolvedMode === 'create') {
|
|
74
|
-
return runCreate(options, warn);
|
|
75
|
-
}
|
|
76
|
-
return runUpdate(options, warn, migrated);
|
|
77
|
-
}
|
|
78
|
-
function resolveMode(requested, specExists) {
|
|
79
|
-
if (requested === 'create')
|
|
80
|
-
return 'create';
|
|
81
|
-
if (requested === 'update')
|
|
82
|
-
return 'update';
|
|
83
|
-
return specExists ? 'update' : 'create';
|
|
84
|
-
}
|
|
85
|
-
/** Build the list of files the new spec would emit, with their content. */
|
|
86
|
-
function planFiles(spec, targetDir) {
|
|
87
|
-
const split = spec.features.powerhouse !== 'Disabled';
|
|
88
|
-
const cliDir = split ? path.join(targetDir, getCliFolderName(spec)) : targetDir;
|
|
89
|
-
const appDir = split ? path.join(targetDir, getAppFolderName(spec)) : null;
|
|
90
|
-
const planned = [];
|
|
91
|
-
const allBuilders = [...CLI_FILE_BUILDERS, ...getProfileFileBuilders(spec)];
|
|
92
|
-
for (const builder of allBuilders) {
|
|
93
|
-
const content = builder.build(spec);
|
|
94
|
-
if (content === null)
|
|
95
|
-
continue;
|
|
96
|
-
const abs = path.join(cliDir, builder.relativePath);
|
|
97
|
-
planned.push({
|
|
98
|
-
relativePath: path.relative(targetDir, abs),
|
|
99
|
-
absolutePath: abs,
|
|
100
|
-
content,
|
|
101
|
-
initOnly: builder.initOnly,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
// Project-root README — always at the top level.
|
|
105
|
-
const readmeAbs = path.join(targetDir, 'README.md');
|
|
106
|
-
planned.push({
|
|
107
|
-
relativePath: 'README.md',
|
|
108
|
-
absolutePath: readmeAbs,
|
|
109
|
-
content: buildReadme(spec),
|
|
7
|
+
return _generateProject({
|
|
8
|
+
...options,
|
|
9
|
+
context: options.context ?? { toolVersion: CLI_VERSION },
|
|
110
10
|
});
|
|
111
|
-
// Publish config — always at the project root (both layouts).
|
|
112
|
-
const publishConfig = path.join(targetDir, 'publish.config.js');
|
|
113
|
-
planned.push({
|
|
114
|
-
relativePath: 'publish.config.js',
|
|
115
|
-
absolutePath: publishConfig,
|
|
116
|
-
content: buildPublishConfigJs(spec),
|
|
117
|
-
});
|
|
118
|
-
// Split-layout only: root package.json + app placeholder files.
|
|
119
|
-
if (split && appDir) {
|
|
120
|
-
const rootPkg = path.join(targetDir, 'package.json');
|
|
121
|
-
planned.push({
|
|
122
|
-
relativePath: 'package.json',
|
|
123
|
-
absolutePath: rootPkg,
|
|
124
|
-
content: buildRootPackageJson(spec),
|
|
125
|
-
});
|
|
126
|
-
const appGitkeep = path.join(appDir, '.gitkeep');
|
|
127
|
-
planned.push({
|
|
128
|
-
relativePath: path.relative(targetDir, appGitkeep),
|
|
129
|
-
absolutePath: appGitkeep,
|
|
130
|
-
content: '',
|
|
131
|
-
});
|
|
132
|
-
const appReadme = path.join(appDir, 'README.md');
|
|
133
|
-
planned.push({
|
|
134
|
-
relativePath: path.relative(targetDir, appReadme),
|
|
135
|
-
absolutePath: appReadme,
|
|
136
|
-
content: buildAppReadmeContent(spec),
|
|
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
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
// Folder rename handling
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
async function fileExists(filePath) {
|
|
160
|
-
try {
|
|
161
|
-
await fs.access(filePath);
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Detect and execute folder renames when the project name changes.
|
|
170
|
-
*
|
|
171
|
-
* Reads the previous `GeneratedState` to find the old folder names, compares
|
|
172
|
-
* with the new spec's folder names. If they differ:
|
|
173
|
-
* 1. Renames the directories on disk.
|
|
174
|
-
* 2. Rewrites hash-record keys from old → new prefixes.
|
|
175
|
-
*
|
|
176
|
-
* Must run BEFORE `planFiles()` so the reconciliation loop sees the files
|
|
177
|
-
* in their new locations.
|
|
178
|
-
*/
|
|
179
|
-
async function handleFolderRename(targetDir, spec, prev, warn) {
|
|
180
|
-
const split = spec.features.powerhouse !== 'Disabled';
|
|
181
|
-
if (!split || !prev || !prev.cliFolderName) {
|
|
182
|
-
return { renamed: false, appInitialized: prev?.appInitialized ?? false };
|
|
183
|
-
}
|
|
184
|
-
const newCliFolder = getCliFolderName(spec);
|
|
185
|
-
const newAppFolder = getAppFolderName(spec);
|
|
186
|
-
const oldCliFolder = prev.cliFolderName;
|
|
187
|
-
const oldAppFolder = prev.appFolderName;
|
|
188
|
-
if (oldCliFolder === newCliFolder && oldAppFolder === newAppFolder) {
|
|
189
|
-
return { renamed: false, appInitialized: prev.appInitialized };
|
|
190
|
-
}
|
|
191
|
-
// Rename CLI folder
|
|
192
|
-
if (oldCliFolder !== newCliFolder) {
|
|
193
|
-
const oldPath = path.join(targetDir, oldCliFolder);
|
|
194
|
-
const newPath = path.join(targetDir, newCliFolder);
|
|
195
|
-
if (await fileExists(oldPath)) {
|
|
196
|
-
if (await fileExists(newPath)) {
|
|
197
|
-
warn(`cannot rename ${oldCliFolder} → ${newCliFolder}: target already exists`);
|
|
198
|
-
return { renamed: false, appInitialized: prev.appInitialized };
|
|
199
|
-
}
|
|
200
|
-
await fs.rename(oldPath, newPath);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// Rename app folder
|
|
204
|
-
if (oldAppFolder !== newAppFolder) {
|
|
205
|
-
const oldPath = path.join(targetDir, oldAppFolder);
|
|
206
|
-
const newPath = path.join(targetDir, newAppFolder);
|
|
207
|
-
if (await fileExists(oldPath)) {
|
|
208
|
-
if (await fileExists(newPath)) {
|
|
209
|
-
warn(`cannot rename ${oldAppFolder} → ${newAppFolder}: target already exists`);
|
|
210
|
-
return { renamed: false, appInitialized: prev.appInitialized };
|
|
211
|
-
}
|
|
212
|
-
await fs.rename(oldPath, newPath);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
// Rewrite hash keys from old folder prefixes to new ones
|
|
216
|
-
const hashes = await readHashes(targetDir);
|
|
217
|
-
const remapped = {};
|
|
218
|
-
for (const [key, value] of Object.entries(hashes)) {
|
|
219
|
-
let newKey = key;
|
|
220
|
-
if (oldCliFolder !== newCliFolder && key.startsWith(oldCliFolder + '/')) {
|
|
221
|
-
newKey = newCliFolder + key.slice(oldCliFolder.length);
|
|
222
|
-
}
|
|
223
|
-
else if (oldAppFolder !== newAppFolder &&
|
|
224
|
-
key.startsWith(oldAppFolder + '/')) {
|
|
225
|
-
newKey = newAppFolder + key.slice(oldAppFolder.length);
|
|
226
|
-
}
|
|
227
|
-
remapped[newKey] = value;
|
|
228
|
-
}
|
|
229
|
-
await writeHashes(targetDir, remapped);
|
|
230
|
-
return { renamed: true, appInitialized: prev.appInitialized };
|
|
231
|
-
}
|
|
232
|
-
/** Create mode — empty target dir, write everything fresh. */
|
|
233
|
-
async function runCreate(options, _warn) {
|
|
234
|
-
const { targetDir, spec } = options;
|
|
235
|
-
if (!options.allowNonEmpty) {
|
|
236
|
-
const empty = await isDirEmptyEnough(targetDir);
|
|
237
|
-
if (!empty) {
|
|
238
|
-
throw new Error(`target directory ${targetDir} is not empty (pass allowNonEmpty or --force to override)`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
const { planned, cliDir, appDir } = planFiles(spec, targetDir);
|
|
242
|
-
const hashes = {};
|
|
243
|
-
const files = [];
|
|
244
|
-
for (const p of planned) {
|
|
245
|
-
await writeFileEnsuringDir(p.absolutePath, p.content);
|
|
246
|
-
hashes[p.relativePath] = hashContent(p.content);
|
|
247
|
-
files.push({ absolutePath: p.absolutePath, relativePath: p.relativePath });
|
|
248
|
-
}
|
|
249
|
-
await writeProjectSpec(targetDir, spec);
|
|
250
|
-
files.push({
|
|
251
|
-
absolutePath: getSpecPath(targetDir),
|
|
252
|
-
relativePath: path.relative(targetDir, getSpecPath(targetDir)),
|
|
253
|
-
});
|
|
254
|
-
await writeHashes(targetDir, hashes);
|
|
255
|
-
// App is not yet initialized on create — ph-init runs as a post-gen action.
|
|
256
|
-
await writeGeneratedState(targetDir, generatedStateFromSpec(spec, false));
|
|
257
|
-
const result = {
|
|
258
|
-
mode: 'create',
|
|
259
|
-
files,
|
|
260
|
-
skipped: [],
|
|
261
|
-
deleted: [],
|
|
262
|
-
migrated: false,
|
|
263
|
-
cliDir,
|
|
264
|
-
appDir,
|
|
265
|
-
pendingActions: [],
|
|
266
|
-
};
|
|
267
|
-
result.pendingActions = await collectPostGenActions(result, spec);
|
|
268
|
-
return result;
|
|
269
|
-
}
|
|
270
|
-
/** Update mode — reconcile against an existing project. */
|
|
271
|
-
async function runUpdate(options, warn, migrated) {
|
|
272
|
-
const { targetDir, spec } = options;
|
|
273
|
-
const force = !!options.force;
|
|
274
|
-
// --- Folder rename detection ---
|
|
275
|
-
const prev = await readGeneratedState(targetDir);
|
|
276
|
-
const { renamed, appInitialized: appWasInitialized } = await handleFolderRename(targetDir, spec, prev, warn);
|
|
277
|
-
const { planned, cliDir, appDir } = planFiles(spec, targetDir);
|
|
278
|
-
const plannedByKey = new Map(planned.map((p) => [p.relativePath, p]));
|
|
279
|
-
const previous = await readHashes(targetDir);
|
|
280
|
-
const next = {};
|
|
281
|
-
const files = [];
|
|
282
|
-
const skipped = [];
|
|
283
|
-
const deleted = [];
|
|
284
|
-
// Step 1: write / patch / skip each planned file.
|
|
285
|
-
for (const p of planned) {
|
|
286
|
-
if (p.initOnly) {
|
|
287
|
-
// User-owned files (e.g. `src/framework.ts` with its `configSchema`)
|
|
288
|
-
// are emitted only on create. In update mode we never overwrite them —
|
|
289
|
-
// but we DO create them if they don't yet exist on disk (e.g. first
|
|
290
|
-
// spec-change trigger run where no explicit `init` was run before).
|
|
291
|
-
const onDisk = await hashFile(p.absolutePath);
|
|
292
|
-
if (onDisk === null && p.content) {
|
|
293
|
-
await writeFileEnsuringDir(p.absolutePath, p.content);
|
|
294
|
-
next[p.relativePath] = hashContent(p.content);
|
|
295
|
-
files.push({ absolutePath: p.absolutePath, relativePath: p.relativePath });
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
const stored = previous[p.relativePath];
|
|
299
|
-
if (stored !== undefined)
|
|
300
|
-
next[p.relativePath] = stored;
|
|
301
|
-
}
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
const existing = await hashFile(p.absolutePath);
|
|
305
|
-
if (existing === null) {
|
|
306
|
-
// Not yet on disk — fresh write.
|
|
307
|
-
await writeFileEnsuringDir(p.absolutePath, p.content);
|
|
308
|
-
next[p.relativePath] = hashContent(p.content);
|
|
309
|
-
files.push({
|
|
310
|
-
absolutePath: p.absolutePath,
|
|
311
|
-
relativePath: p.relativePath,
|
|
312
|
-
});
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
// File exists. Decide how to update it.
|
|
316
|
-
if (hasMarkers(p.content)) {
|
|
317
|
-
const existingContent = await fs.readFile(p.absolutePath, 'utf8');
|
|
318
|
-
const patched = hasMarkers(existingContent)
|
|
319
|
-
? spliceMarkerRegions(existingContent, p.content)
|
|
320
|
-
: p.content;
|
|
321
|
-
if (patched === existingContent) {
|
|
322
|
-
// No-op: regions already match.
|
|
323
|
-
next[p.relativePath] = hashContent(patched);
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
await writeFileEnsuringDir(p.absolutePath, patched);
|
|
327
|
-
next[p.relativePath] = hashContent(patched);
|
|
328
|
-
files.push({
|
|
329
|
-
absolutePath: p.absolutePath,
|
|
330
|
-
relativePath: p.relativePath,
|
|
331
|
-
});
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
// No markers — hash-protected overwrite.
|
|
335
|
-
const stored = previous[p.relativePath];
|
|
336
|
-
const pristine = stored !== undefined && stored === existing;
|
|
337
|
-
if (!pristine && !force) {
|
|
338
|
-
// User edited a managed file. Leave it alone.
|
|
339
|
-
warn(`skipped ${p.relativePath} — modified since last generation. Use --force to overwrite.`);
|
|
340
|
-
skipped.push({
|
|
341
|
-
absolutePath: p.absolutePath,
|
|
342
|
-
relativePath: p.relativePath,
|
|
343
|
-
});
|
|
344
|
-
// Preserve the previous hash so the file stays on our ledger.
|
|
345
|
-
if (stored !== undefined)
|
|
346
|
-
next[p.relativePath] = stored;
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
if (existing === hashContent(p.content)) {
|
|
350
|
-
// Already up to date.
|
|
351
|
-
next[p.relativePath] = existing;
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
await writeFileEnsuringDir(p.absolutePath, p.content);
|
|
355
|
-
next[p.relativePath] = hashContent(p.content);
|
|
356
|
-
files.push({
|
|
357
|
-
absolutePath: p.absolutePath,
|
|
358
|
-
relativePath: p.relativePath,
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
// Step 2: delete files that the new spec no longer emits, IF we still
|
|
362
|
-
// recognise their content (hash match). Otherwise warn and keep.
|
|
363
|
-
for (const [relPath, storedHash] of Object.entries(previous)) {
|
|
364
|
-
if (plannedByKey.has(relPath))
|
|
365
|
-
continue;
|
|
366
|
-
const abs = path.join(targetDir, relPath);
|
|
367
|
-
const onDisk = await hashFile(abs);
|
|
368
|
-
if (onDisk === null)
|
|
369
|
-
continue; // already gone
|
|
370
|
-
if (onDisk !== storedHash && !force) {
|
|
371
|
-
warn(`kept ${relPath} — modified since last generation; new spec no longer emits it.`);
|
|
372
|
-
next[relPath] = storedHash;
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
await fs.rm(abs, { force: true });
|
|
376
|
-
deleted.push({ absolutePath: abs, relativePath: relPath });
|
|
377
|
-
}
|
|
378
|
-
await writeProjectSpec(targetDir, spec);
|
|
379
|
-
await writeHashes(targetDir, next);
|
|
380
|
-
// Re-patch app package.json if name or scope changed and app is initialized.
|
|
381
|
-
// Add to `files` so collectPostGenActions sees the change and triggers
|
|
382
|
-
// app-install → app-build → cli-install → cli-build.
|
|
383
|
-
const appIsInitialized = appDir
|
|
384
|
-
? await fileExists(path.join(appDir, 'package.json'))
|
|
385
|
-
: false;
|
|
386
|
-
const nameOrScopeChanged = prev
|
|
387
|
-
? prev.name !== spec.name || prev.scope !== spec.scope
|
|
388
|
-
: false;
|
|
389
|
-
if (appDir && appIsInitialized && nameOrScopeChanged) {
|
|
390
|
-
const patched = await patchAppPackageName(appDir, spec, warn);
|
|
391
|
-
if (patched) {
|
|
392
|
-
const appFolder = path.basename(appDir);
|
|
393
|
-
files.push({
|
|
394
|
-
absolutePath: path.join(appDir, 'package.json'),
|
|
395
|
-
relativePath: path.join(appFolder, 'package.json'),
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
await writeGeneratedState(targetDir, generatedStateFromSpec(spec, appIsInitialized));
|
|
400
|
-
const result = {
|
|
401
|
-
mode: 'update',
|
|
402
|
-
files,
|
|
403
|
-
skipped,
|
|
404
|
-
deleted,
|
|
405
|
-
migrated,
|
|
406
|
-
cliDir,
|
|
407
|
-
appDir,
|
|
408
|
-
pendingActions: [],
|
|
409
|
-
};
|
|
410
|
-
result.pendingActions = await collectPostGenActions(result, spec);
|
|
411
|
-
return result;
|
|
412
11
|
}
|
|
@@ -1,36 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Marker-region parsing for `@clint:begin {name}` / `@clint:end {name}`
|
|
3
|
-
* comment blocks. Used in delta mode to splice freshly-generated regions
|
|
4
|
-
* into an existing, user-edited file while preserving everything outside
|
|
5
|
-
* the markers.
|
|
6
|
-
*
|
|
7
|
-
* The only syntactic requirement is that the markers are on their own lines,
|
|
8
|
-
* as ordinary line comments (`//`). Indentation is preserved for output.
|
|
9
|
-
*/
|
|
10
|
-
export interface MarkerRegion {
|
|
11
|
-
/** Region identifier (the text after `@clint:begin`). */
|
|
12
|
-
name: string;
|
|
13
|
-
/** Lines *between* the begin and end markers (exclusive). */
|
|
14
|
-
body: string[];
|
|
15
|
-
/** Indentation of the begin line — used when rewriting. */
|
|
16
|
-
indent: string;
|
|
17
|
-
/** Line index of the begin marker in the source. */
|
|
18
|
-
beginLine: number;
|
|
19
|
-
/** Line index of the end marker in the source. */
|
|
20
|
-
endLine: number;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Parse every `@clint:begin {name}` … `@clint:end {name}` block in `source`.
|
|
24
|
-
* Throws if markers are mismatched or unclosed.
|
|
25
|
-
*/
|
|
26
|
-
export declare function parseMarkerRegions(source: string): MarkerRegion[];
|
|
27
|
-
/**
|
|
28
|
-
* Replace every region in `target` that also exists in `source` with the
|
|
29
|
-
* `source` region's body. Leaves regions present in `target` but not `source`
|
|
30
|
-
* untouched. Ignores regions in `source` missing from `target`.
|
|
31
|
-
*
|
|
32
|
-
* Returns the rewritten file content.
|
|
33
|
-
*/
|
|
34
|
-
export declare function spliceMarkerRegions(target: string, source: string): string;
|
|
35
|
-
/** Convenience — true if `content` contains at least one `@clint:begin`. */
|
|
36
|
-
export declare function hasMarkers(content: string): boolean;
|
|
1
|
+
export { type MarkerRegion, parseMarkerRegions, spliceMarkerRegions, hasMarkers, } from '@powerhousedao/ph-clint-dev/codegen/markers';
|
package/dist/codegen/markers.js
CHANGED
|
@@ -1,86 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Marker-region parsing for `@clint:begin {name}` / `@clint:end {name}`
|
|
3
|
-
* comment blocks. Used in delta mode to splice freshly-generated regions
|
|
4
|
-
* into an existing, user-edited file while preserving everything outside
|
|
5
|
-
* the markers.
|
|
6
|
-
*
|
|
7
|
-
* The only syntactic requirement is that the markers are on their own lines,
|
|
8
|
-
* as ordinary line comments (`//`). Indentation is preserved for output.
|
|
9
|
-
*/
|
|
10
|
-
const BEGIN_RE = /^([ \t]*)\/\/\s*@clint:begin\s+(\S+)\s*$/;
|
|
11
|
-
const END_RE = /^([ \t]*)\/\/\s*@clint:end\s+(\S+)\s*$/;
|
|
12
|
-
/**
|
|
13
|
-
* Parse every `@clint:begin {name}` … `@clint:end {name}` block in `source`.
|
|
14
|
-
* Throws if markers are mismatched or unclosed.
|
|
15
|
-
*/
|
|
16
|
-
export function parseMarkerRegions(source) {
|
|
17
|
-
const lines = source.split('\n');
|
|
18
|
-
const regions = [];
|
|
19
|
-
let open = null;
|
|
20
|
-
let bodyStart = 0;
|
|
21
|
-
for (let i = 0; i < lines.length; i++) {
|
|
22
|
-
const line = lines[i];
|
|
23
|
-
const begin = BEGIN_RE.exec(line);
|
|
24
|
-
if (begin) {
|
|
25
|
-
if (open) {
|
|
26
|
-
throw new Error(`nested @clint:begin ${begin[2]} at line ${i + 1} while ${open.name} is still open`);
|
|
27
|
-
}
|
|
28
|
-
open = { name: begin[2], indent: begin[1], beginLine: i };
|
|
29
|
-
bodyStart = i + 1;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
const end = END_RE.exec(line);
|
|
33
|
-
if (end) {
|
|
34
|
-
if (!open) {
|
|
35
|
-
throw new Error(`@clint:end ${end[2]} at line ${i + 1} without matching begin`);
|
|
36
|
-
}
|
|
37
|
-
if (open.name !== end[2]) {
|
|
38
|
-
throw new Error(`@clint:end ${end[2]} at line ${i + 1} does not match open @clint:begin ${open.name}`);
|
|
39
|
-
}
|
|
40
|
-
regions.push({
|
|
41
|
-
name: open.name,
|
|
42
|
-
body: lines.slice(bodyStart, i),
|
|
43
|
-
indent: open.indent,
|
|
44
|
-
beginLine: open.beginLine,
|
|
45
|
-
endLine: i,
|
|
46
|
-
});
|
|
47
|
-
open = null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (open) {
|
|
51
|
-
throw new Error(`unterminated @clint:begin ${open.name}`);
|
|
52
|
-
}
|
|
53
|
-
return regions;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Replace every region in `target` that also exists in `source` with the
|
|
57
|
-
* `source` region's body. Leaves regions present in `target` but not `source`
|
|
58
|
-
* untouched. Ignores regions in `source` missing from `target`.
|
|
59
|
-
*
|
|
60
|
-
* Returns the rewritten file content.
|
|
61
|
-
*/
|
|
62
|
-
export function spliceMarkerRegions(target, source) {
|
|
63
|
-
const sourceRegions = parseMarkerRegions(source);
|
|
64
|
-
const sourceByName = new Map();
|
|
65
|
-
for (const r of sourceRegions)
|
|
66
|
-
sourceByName.set(r.name, r);
|
|
67
|
-
const targetRegions = parseMarkerRegions(target);
|
|
68
|
-
if (targetRegions.length === 0)
|
|
69
|
-
return target;
|
|
70
|
-
const lines = target.split('\n');
|
|
71
|
-
// Apply replacements from last to first so line indices stay valid.
|
|
72
|
-
const sorted = [...targetRegions].sort((a, b) => b.beginLine - a.beginLine);
|
|
73
|
-
for (const region of sorted) {
|
|
74
|
-
const fresh = sourceByName.get(region.name);
|
|
75
|
-
if (!fresh)
|
|
76
|
-
continue;
|
|
77
|
-
// Replace the body lines (exclusive of the begin/end marker lines).
|
|
78
|
-
const bodyLen = region.endLine - region.beginLine - 1;
|
|
79
|
-
lines.splice(region.beginLine + 1, bodyLen, ...fresh.body);
|
|
80
|
-
}
|
|
81
|
-
return lines.join('\n');
|
|
82
|
-
}
|
|
83
|
-
/** Convenience — true if `content` contains at least one `@clint:begin`. */
|
|
84
|
-
export function hasMarkers(content) {
|
|
85
|
-
return /^[ \t]*\/\/\s*@clint:begin\s+\S+\s*$/m.test(content);
|
|
86
|
-
}
|
|
1
|
+
export { parseMarkerRegions, spliceMarkerRegions, hasMarkers, } from '@powerhousedao/ph-clint-dev/codegen/markers';
|
|
@@ -1,72 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { ProcessRunOptions } from '@powerhousedao/ph-clint';
|
|
3
|
-
/**
|
|
4
|
-
* Signature for the subprocess runner — matches CommandContext.runProcess.
|
|
5
|
-
*/
|
|
6
|
-
type RunProcess = (command: string, opts?: Omit<ProcessRunOptions, 'onOutput'>) => Promise<{
|
|
7
|
-
success: boolean;
|
|
8
|
-
output: string;
|
|
9
|
-
}>;
|
|
10
|
-
export type { RunProcess };
|
|
11
|
-
export interface PhInitOptions {
|
|
12
|
-
/** Absolute path to the project root. */
|
|
13
|
-
targetDir: string;
|
|
14
|
-
/** Absolute path to `{targetDir}/{name}-app` (exists with our placeholder). */
|
|
15
|
-
appDir: string;
|
|
16
|
-
spec: ClintProjectSpec;
|
|
17
|
-
/** Logger; defaults to noop. */
|
|
18
|
-
log?: (msg: string) => void;
|
|
19
|
-
/** Override `ph` binary name (for tests). */
|
|
20
|
-
binName?: string;
|
|
21
|
-
/** Subprocess runner — matches CommandContext.runProcess. */
|
|
22
|
-
runProcess: RunProcess;
|
|
23
|
-
/** Explicit Powerhouse version to pin `ph init` to (tag or semver). */
|
|
24
|
-
phVersion?: string;
|
|
25
|
-
}
|
|
26
|
-
export interface PhInitResult {
|
|
27
|
-
ran: boolean;
|
|
28
|
-
exitCode?: number;
|
|
29
|
-
reason?: string;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Detect the installed `ph` CLI version by parsing `ph --version` output.
|
|
33
|
-
* Returns the version string (e.g. '6.0.0-dev.194') or undefined if
|
|
34
|
-
* detection fails.
|
|
35
|
-
*/
|
|
36
|
-
export declare function getPhVersion(binName: string): string | undefined;
|
|
37
|
-
export declare function runPhInit(options: PhInitOptions): Promise<PhInitResult>;
|
|
38
|
-
/**
|
|
39
|
-
* Patch the app `package.json` name to the scoped form (`@scope/name-app`).
|
|
40
|
-
* Called after `ph init` and after name/scope renames.
|
|
41
|
-
*
|
|
42
|
-
* Returns `true` if the file was actually changed.
|
|
43
|
-
*/
|
|
44
|
-
export declare function patchAppPackageName(appDir: string, spec: {
|
|
45
|
-
name: string;
|
|
46
|
-
scope?: string;
|
|
47
|
-
}, log?: (msg: string) => void): Promise<boolean>;
|
|
48
|
-
/**
|
|
49
|
-
* Run `ph install <packages> --local --pnpm` in the app directory to register
|
|
50
|
-
* external packages in `powerhouse.config.json`.
|
|
51
|
-
*/
|
|
52
|
-
export declare function runPhInstallPackages(options: {
|
|
53
|
-
appDir: string;
|
|
54
|
-
packages: string[];
|
|
55
|
-
log?: (msg: string) => void;
|
|
56
|
-
runProcess: RunProcess;
|
|
57
|
-
}): Promise<boolean>;
|
|
58
|
-
export interface PnpmInstallOptions {
|
|
59
|
-
/** Directories to run `pnpm install` in, in order. */
|
|
60
|
-
dirs: string[];
|
|
61
|
-
log?: (msg: string) => void;
|
|
62
|
-
/** Override `pnpm` binary name (for tests). */
|
|
63
|
-
binName?: string;
|
|
64
|
-
/** Subprocess runner — matches CommandContext.runProcess. */
|
|
65
|
-
runProcess: RunProcess;
|
|
66
|
-
}
|
|
67
|
-
export interface PnpmInstallResult {
|
|
68
|
-
ran: string[];
|
|
69
|
-
skipped: string[];
|
|
70
|
-
reason?: string;
|
|
71
|
-
}
|
|
72
|
-
export declare function runPnpmInstall(options: PnpmInstallOptions): Promise<PnpmInstallResult>;
|
|
1
|
+
export { type RunProcess, type PhInitOptions, type PhInitResult, getPhVersion, runPhInit, patchAppPackageName, type PnpmInstallOptions, type PnpmInstallResult, runPnpmInstall, runPhInstallPackages, } from '@powerhousedao/ph-clint-dev/codegen/scaffold';
|