@pareto4/pp-core 1.11.0 → 1.11.1
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/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pp-core",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "PP Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads pp-core's operating context into every Gemini CLI session.",
|
|
5
5
|
"contextFileName": "GEMINI.md"
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pareto4/pp-core",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "Pareto pp-core — The Pareto Project's AI-assisted engineering framework: meta-prompting, context engineering, and spec-driven development for AI coding agents.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pp-core": "bin/install.js",
|
|
@@ -46,6 +46,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
48
|
exports.COMPOSE_BINDINGS_FLAGS = exports.SEED_VALUE_MAX_LENGTH = void 0;
|
|
49
|
+
exports._resetPlatformTagMemoForTests = _resetPlatformTagMemoForTests;
|
|
49
50
|
exports.composeBindings = composeBindings;
|
|
50
51
|
exports.cmdComposeBindings = cmdComposeBindings;
|
|
51
52
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -336,6 +337,22 @@ function readSeed(cwd, opts) {
|
|
|
336
337
|
return { values, profile, warnings };
|
|
337
338
|
}
|
|
338
339
|
// ─── resolveLatestPlatformTag (D-04) ─────────────────────────────────────────
|
|
340
|
+
/**
|
|
341
|
+
* Per-process memo for the D-04 tag lookup, keyed by the resolved URL, holding
|
|
342
|
+
* the full `{ tag, warnings }` verdict — successes AND failures. One real
|
|
343
|
+
* `pp-tools init` run calls `composeBindings` twice (the dry-run preview in
|
|
344
|
+
* `computeCurrentStep`, then the write-mode call after `compose_confirm`), so
|
|
345
|
+
* without this memo a decline costs a network round-trip for nothing and a
|
|
346
|
+
* failed lookup would print its bounded warning twice.
|
|
347
|
+
*/
|
|
348
|
+
const platformTagMemo = new Map();
|
|
349
|
+
/** Test-only seam: clear the per-process platform.tag lookup memo so a test
|
|
350
|
+
* suite that shares a resolved URL across cases sees an empty memo each time
|
|
351
|
+
* (`init-tag-lookup-raw-git-error`). Mirrors the reset-seam convention at
|
|
352
|
+
* `src/active-workstream-store.cts`'s controlling-TTY probe cache. */
|
|
353
|
+
function _resetPlatformTagMemoForTests() {
|
|
354
|
+
platformTagMemo.clear();
|
|
355
|
+
}
|
|
339
356
|
/**
|
|
340
357
|
* Resolve the latest STABLE tag for `repo` over the resolved host, never
|
|
341
358
|
* shell-eval'd (T-34-25): `git ls-remote` is spawned with an argv array via
|
|
@@ -344,6 +361,13 @@ function readSeed(cwd, opts) {
|
|
|
344
361
|
* else `defaultHostForProvider(provider)` — the host is authoritative and
|
|
345
362
|
* never derived from the repo string (Phase 31 D-09).
|
|
346
363
|
*
|
|
364
|
+
* The child's stderr is piped into the parent's buffer rather than inherited
|
|
365
|
+
* — an explicit `stdio: ['ignore', 'pipe', 'pipe']` — and deliberately
|
|
366
|
+
* discarded rather than logged: the bounded warning this function returns on
|
|
367
|
+
* failure is the single user-visible surface for a lookup failure. Inheriting
|
|
368
|
+
* the child's stream let a raw git `fatal:` line reach the terminal during an
|
|
369
|
+
* otherwise successful first-run experience (`init-tag-lookup-raw-git-error`).
|
|
370
|
+
*
|
|
347
371
|
* Every candidate ref is filtered with `isStableTripletSemver` and ordered
|
|
348
372
|
* with `compareSemverCore` — the single blessed comparison source, never
|
|
349
373
|
* reimplemented. An empty result degrades to `{ tag: null, warnings: [] }`
|
|
@@ -353,13 +377,34 @@ function readSeed(cwd, opts) {
|
|
|
353
377
|
* platform being unpinned, never a compose failure.
|
|
354
378
|
*/
|
|
355
379
|
function resolveLatestPlatformTag(cwd, repo, provider, opts) {
|
|
380
|
+
let url;
|
|
356
381
|
try {
|
|
357
382
|
const hostRes = (0, vault_coordinates_cjs_1.resolvePlatformHost)(cwd);
|
|
358
383
|
const host = hostRes.host ?? (0, vault_coordinates_cjs_1.defaultHostForProvider)(provider);
|
|
359
|
-
|
|
384
|
+
url = `https://${host}/${repo}`;
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
return {
|
|
388
|
+
tag: null,
|
|
389
|
+
warnings: [
|
|
390
|
+
bound('platform.tag could not be resolved — the tag lookup failed or was unreachable; the platform is unpinned.'),
|
|
391
|
+
],
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const cached = platformTagMemo.get(url);
|
|
395
|
+
if (cached)
|
|
396
|
+
return cached;
|
|
397
|
+
let result;
|
|
398
|
+
try {
|
|
360
399
|
const lsOutput = typeof opts._lsRemote === 'function'
|
|
361
400
|
? opts._lsRemote(url)
|
|
362
|
-
: node_child_process_1.default.execFileSync('git', ['ls-remote', '--tags', '--refs', '--sort=-v:refname', url, 'v*'], {
|
|
401
|
+
: node_child_process_1.default.execFileSync('git', ['ls-remote', '--tags', '--refs', '--sort=-v:refname', url, 'v*'], {
|
|
402
|
+
encoding: 'utf-8',
|
|
403
|
+
timeout: 10_000,
|
|
404
|
+
maxBuffer: 512 * 1024,
|
|
405
|
+
windowsHide: true,
|
|
406
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
407
|
+
});
|
|
363
408
|
const tags = lsOutput
|
|
364
409
|
.split('\n')
|
|
365
410
|
.map((line) => {
|
|
@@ -368,23 +413,27 @@ function resolveLatestPlatformTag(cwd, repo, provider, opts) {
|
|
|
368
413
|
})
|
|
369
414
|
.filter((t) => t && (0, semver_compare_cjs_1.isStableTripletSemver)(t));
|
|
370
415
|
if (tags.length === 0) {
|
|
371
|
-
|
|
416
|
+
result = { tag: null, warnings: [] };
|
|
372
417
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
latest
|
|
418
|
+
else {
|
|
419
|
+
let latest = tags[0];
|
|
420
|
+
for (const t of tags) {
|
|
421
|
+
if ((0, semver_compare_cjs_1.compareSemverCore)(t, latest) > 0)
|
|
422
|
+
latest = t;
|
|
423
|
+
}
|
|
424
|
+
result = { tag: latest, warnings: [] };
|
|
377
425
|
}
|
|
378
|
-
return { tag: latest, warnings: [] };
|
|
379
426
|
}
|
|
380
427
|
catch {
|
|
381
|
-
|
|
428
|
+
result = {
|
|
382
429
|
tag: null,
|
|
383
430
|
warnings: [
|
|
384
431
|
bound('platform.tag could not be resolved — the tag lookup failed or was unreachable; the platform is unpinned.'),
|
|
385
432
|
],
|
|
386
433
|
};
|
|
387
434
|
}
|
|
435
|
+
platformTagMemo.set(url, result);
|
|
436
|
+
return result;
|
|
388
437
|
}
|
|
389
438
|
/**
|
|
390
439
|
* Read the first readable, semver-shaped `VERSION` file among the installed
|
|
@@ -165,6 +165,14 @@ function getDotted(obj, dottedPath) {
|
|
|
165
165
|
* `optionalAnswer: true` — that flag describes the ANSWER (pressing Enter
|
|
166
166
|
* is a complete, valid response), never whether the question is asked.
|
|
167
167
|
*
|
|
168
|
+
* Question 6 (`org_profile`)'s `preselected` is resolved via
|
|
169
|
+
* `resolveDetectedOrgProfile` — set only when exactly one org-profile-shaped
|
|
170
|
+
* package is installed (INIT-PATCH-C); the prompt text is built from that
|
|
171
|
+
* detected value rather than kept as two hard-coded strings that can drift.
|
|
172
|
+
* `opts._globalRoot` is forwarded to it unchanged, the same test-seam
|
|
173
|
+
* convention `verifyInitInvariants`'s own `_globalRoot` already follows —
|
|
174
|
+
* production callers omit it.
|
|
175
|
+
*
|
|
168
176
|
* Question 7 (`provider`) carries the fixed, complete option list: every
|
|
169
177
|
* value in `PROVIDERS` (imported, never retyped — this is what keeps
|
|
170
178
|
* `azure` from being silently dropped again, T-34-50) plus the three
|
|
@@ -173,7 +181,7 @@ function getDotted(obj, dottedPath) {
|
|
|
173
181
|
* probe reads (pure config + org-profile consultation, no network) — so
|
|
174
182
|
* calling it directly here is equivalent, not a second implementation.
|
|
175
183
|
*/
|
|
176
|
-
function buildInitQuestions(cwd) {
|
|
184
|
+
function buildInitQuestions(cwd, opts = {}) {
|
|
177
185
|
let preselected = git_provider_cjs_1.DEFAULT_PROVIDER;
|
|
178
186
|
try {
|
|
179
187
|
preselected = (0, git_provider_cjs_1.resolveProvider)(cwd).provider;
|
|
@@ -182,6 +190,29 @@ function buildInitQuestions(cwd) {
|
|
|
182
190
|
preselected = git_provider_cjs_1.DEFAULT_PROVIDER;
|
|
183
191
|
}
|
|
184
192
|
const providerOptions = Object.freeze([...git_provider_cjs_1.PROVIDERS, ...IDENTITY_ONLY_PROVIDER_OPTIONS]);
|
|
193
|
+
const detectedOrgProfile = resolveDetectedOrgProfile(cwd, opts);
|
|
194
|
+
// The template example is written as an angle-bracket placeholder, not a
|
|
195
|
+
// literal package name. `tests/org-profile.test.cjs`'s client-confidentiality
|
|
196
|
+
// guard scans this file for any concrete scope-slash-org-prefix package
|
|
197
|
+
// identifier, so even a generic literal placeholder trips it — the template
|
|
198
|
+
// keeps the naming convention visible while staying clean, and the guard
|
|
199
|
+
// stays maximally strict, no allowlist entry was added for it. The detected
|
|
200
|
+
// value itself is never a literal in this file — it is a runtime string
|
|
201
|
+
// read from an installed package, interpolated into the prompt below.
|
|
202
|
+
const orgProfileQuestion = isUsableString(detectedOrgProfile)
|
|
203
|
+
? {
|
|
204
|
+
id: 'org_profile',
|
|
205
|
+
prompt: `Org profile package (optional — a single installed org profile was detected: "${detectedOrgProfile}". ` +
|
|
206
|
+
'Press Enter to use it, type "none" to skip it, or type a different package name.)',
|
|
207
|
+
optionalAnswer: true,
|
|
208
|
+
preselected: detectedOrgProfile,
|
|
209
|
+
preselectedLabel: `${detectedOrgProfile} (detected installed profile)`,
|
|
210
|
+
}
|
|
211
|
+
: {
|
|
212
|
+
id: 'org_profile',
|
|
213
|
+
prompt: 'Org profile package (optional — press Enter to skip, e.g. "@<your-scope>/org-<name>")',
|
|
214
|
+
optionalAnswer: true,
|
|
215
|
+
};
|
|
185
216
|
return [
|
|
186
217
|
{ id: 'project_name', prompt: 'Project name (human-readable, e.g. "Acme Dashboard")', optionalAnswer: false },
|
|
187
218
|
{ id: 'project_slug', prompt: 'Project slug (kebab-case, e.g. "acme-dashboard")', optionalAnswer: false },
|
|
@@ -196,17 +227,7 @@ function buildInitQuestions(cwd) {
|
|
|
196
227
|
optionalAnswer: false,
|
|
197
228
|
},
|
|
198
229
|
{ id: 'description', prompt: 'One-line description (what this project does)', optionalAnswer: false },
|
|
199
|
-
|
|
200
|
-
id: 'org_profile',
|
|
201
|
-
// The example is written as an angle-bracket TEMPLATE, not a literal package
|
|
202
|
-
// name. `tests/org-profile.test.cjs`'s client-confidentiality guard scans this
|
|
203
|
-
// file for any concrete scope-slash-org-prefix package identifier, so even a
|
|
204
|
-
// generic literal placeholder trips it. The template keeps the naming
|
|
205
|
-
// convention visible while staying clean, and the guard stays maximally
|
|
206
|
-
// strict — no allowlist entry was added for it.
|
|
207
|
-
prompt: 'Org profile package (optional — press Enter to skip, e.g. "@<your-scope>/org-<name>")',
|
|
208
|
-
optionalAnswer: true,
|
|
209
|
-
},
|
|
230
|
+
orgProfileQuestion,
|
|
210
231
|
{
|
|
211
232
|
id: 'provider',
|
|
212
233
|
prompt: 'Provider (gitlab / azure / github / other / none)',
|
|
@@ -249,30 +270,47 @@ function resolveGlobalNodeModulesRoot() {
|
|
|
249
270
|
return null;
|
|
250
271
|
}
|
|
251
272
|
}
|
|
273
|
+
/** Conservative npm-package-name shape check (bare `name` or scoped
|
|
274
|
+
* `@scope/name`; lowercase, digits, `.`/`_`/`-`, never leading `.`/`_` in
|
|
275
|
+
* either segment). A directory name that does not match is never offered as
|
|
276
|
+
* a detected default (T-QP-02): it cannot be a resolvable package pointer,
|
|
277
|
+
* so rejecting it here keeps a hostile directory name (control characters,
|
|
278
|
+
* terminal escapes) from ever reaching a rendered prompt. */
|
|
279
|
+
const NPM_PACKAGE_NAME_SHAPE_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
252
280
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* `ORG_PROFILE_DATA_FILE` at its root
|
|
256
|
-
* `
|
|
257
|
-
*
|
|
258
|
-
* check
|
|
281
|
+
* Walk `nodeModulesDir` exactly like the boolean check below (one level, or
|
|
282
|
+
* one level inside an `@scope/` directory), collecting the name of every
|
|
283
|
+
* package directory carrying `ORG_PROFILE_DATA_FILE` at its root instead of
|
|
284
|
+
* stopping at the first hit. `opts.limit`, when set, stops the walk once
|
|
285
|
+
* that many names have been collected — passing `1` reproduces the boolean
|
|
286
|
+
* check's early-exit behaviour exactly. Bounded at `NODE_MODULES_SCAN_CAP`
|
|
287
|
+
* total directory entries visited (T-34-54: this check must never hang on a
|
|
288
|
+
* pathological `node_modules` tree).
|
|
259
289
|
*
|
|
260
290
|
* This is a heuristic, not a resolution: it answers "a package shaped like
|
|
261
291
|
* an org profile is installed here", not "this exact profile would
|
|
262
|
-
* resolve" —
|
|
292
|
+
* resolve" — callers never claim more than that.
|
|
263
293
|
*/
|
|
264
|
-
function
|
|
294
|
+
function collectOrgProfileShapedPackages(nodeModulesDir, opts = {}) {
|
|
295
|
+
const names = [];
|
|
265
296
|
let visited = 0;
|
|
297
|
+
const limitReached = () => opts.limit !== undefined && names.length >= opts.limit;
|
|
298
|
+
const addIfShaped = (name) => {
|
|
299
|
+
if (NPM_PACKAGE_NAME_SHAPE_RE.test(name))
|
|
300
|
+
names.push(name);
|
|
301
|
+
};
|
|
266
302
|
let entries;
|
|
267
303
|
try {
|
|
268
304
|
entries = node_fs_1.default.readdirSync(nodeModulesDir, { withFileTypes: true });
|
|
269
305
|
}
|
|
270
306
|
catch {
|
|
271
|
-
return false;
|
|
307
|
+
return { names, capped: false };
|
|
272
308
|
}
|
|
273
309
|
for (const entry of entries) {
|
|
310
|
+
if (limitReached())
|
|
311
|
+
return { names, capped: false };
|
|
274
312
|
if (visited >= NODE_MODULES_SCAN_CAP)
|
|
275
|
-
return
|
|
313
|
+
return { names, capped: true };
|
|
276
314
|
visited += 1;
|
|
277
315
|
if (!entry.isDirectory())
|
|
278
316
|
continue;
|
|
@@ -286,14 +324,17 @@ function containsOrgProfileShapedPackage(nodeModulesDir) {
|
|
|
286
324
|
continue;
|
|
287
325
|
}
|
|
288
326
|
for (const scopedEntry of scoped) {
|
|
327
|
+
if (limitReached())
|
|
328
|
+
return { names, capped: false };
|
|
289
329
|
if (visited >= NODE_MODULES_SCAN_CAP)
|
|
290
|
-
return
|
|
330
|
+
return { names, capped: true };
|
|
291
331
|
visited += 1;
|
|
292
332
|
if (!scopedEntry.isDirectory())
|
|
293
333
|
continue;
|
|
294
334
|
try {
|
|
295
|
-
if (node_fs_1.default.existsSync(node_path_1.default.join(scopeDir, scopedEntry.name, org_profile_cjs_1.ORG_PROFILE_DATA_FILE)))
|
|
296
|
-
|
|
335
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(scopeDir, scopedEntry.name, org_profile_cjs_1.ORG_PROFILE_DATA_FILE))) {
|
|
336
|
+
addIfShaped(`${entry.name}/${scopedEntry.name}`);
|
|
337
|
+
}
|
|
297
338
|
}
|
|
298
339
|
catch {
|
|
299
340
|
continue;
|
|
@@ -302,15 +343,27 @@ function containsOrgProfileShapedPackage(nodeModulesDir) {
|
|
|
302
343
|
}
|
|
303
344
|
else {
|
|
304
345
|
try {
|
|
305
|
-
if (node_fs_1.default.existsSync(node_path_1.default.join(nodeModulesDir, entry.name, org_profile_cjs_1.ORG_PROFILE_DATA_FILE)))
|
|
306
|
-
|
|
346
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(nodeModulesDir, entry.name, org_profile_cjs_1.ORG_PROFILE_DATA_FILE))) {
|
|
347
|
+
addIfShaped(entry.name);
|
|
348
|
+
}
|
|
307
349
|
}
|
|
308
350
|
catch {
|
|
309
351
|
continue;
|
|
310
352
|
}
|
|
311
353
|
}
|
|
312
354
|
}
|
|
313
|
-
return false;
|
|
355
|
+
return { names, capped: false };
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* True when `nodeModulesDir` contains at least one package directory (one
|
|
359
|
+
* level, or one level inside an `@scope/` directory) carrying a file named
|
|
360
|
+
* `ORG_PROFILE_DATA_FILE` at its root — the same fixed filename
|
|
361
|
+
* `resolveOrgProfileDocument` reads, imported rather than retyped. Reduced to
|
|
362
|
+
* the collecting walk above with `limit: 1`, preserving its early-exit
|
|
363
|
+
* behaviour exactly.
|
|
364
|
+
*/
|
|
365
|
+
function containsOrgProfileShapedPackage(nodeModulesDir) {
|
|
366
|
+
return collectOrgProfileShapedPackages(nodeModulesDir, { limit: 1 }).names.length > 0;
|
|
314
367
|
}
|
|
315
368
|
function orgProfilePackageResolvable(cwd, opts) {
|
|
316
369
|
try {
|
|
@@ -328,6 +381,44 @@ function orgProfilePackageResolvable(cwd, opts) {
|
|
|
328
381
|
return false;
|
|
329
382
|
}
|
|
330
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Detect a single, unambiguous installed org-profile-shaped package
|
|
386
|
+
* (INIT-PATCH-C): collects from the project's `node_modules` and — using the
|
|
387
|
+
* same `opts._globalRoot` override convention `orgProfilePackageResolvable`
|
|
388
|
+
* already follows — the global root, de-duplicates by package name across
|
|
389
|
+
* both roots, and returns that single name only when exactly one distinct
|
|
390
|
+
* name was found and NEITHER scan reported `capped`. A capped scan is
|
|
391
|
+
* inconclusive and must never yield a default (T-QP-03). Two or more
|
|
392
|
+
* distinct names, or none, returns null. Never throws.
|
|
393
|
+
*/
|
|
394
|
+
function resolveDetectedOrgProfile(cwd, opts) {
|
|
395
|
+
try {
|
|
396
|
+
const names = new Set();
|
|
397
|
+
let anyCapped = false;
|
|
398
|
+
const projectNodeModules = node_path_1.default.join(cwd, 'node_modules');
|
|
399
|
+
if (node_fs_1.default.existsSync(projectNodeModules)) {
|
|
400
|
+
const projectResult = collectOrgProfileShapedPackages(projectNodeModules);
|
|
401
|
+
if (projectResult.capped)
|
|
402
|
+
anyCapped = true;
|
|
403
|
+
for (const name of projectResult.names)
|
|
404
|
+
names.add(name);
|
|
405
|
+
}
|
|
406
|
+
const globalRoot = opts._globalRoot !== undefined ? opts._globalRoot : resolveGlobalNodeModulesRoot();
|
|
407
|
+
if (globalRoot) {
|
|
408
|
+
const globalResult = collectOrgProfileShapedPackages(globalRoot);
|
|
409
|
+
if (globalResult.capped)
|
|
410
|
+
anyCapped = true;
|
|
411
|
+
for (const name of globalResult.names)
|
|
412
|
+
names.add(name);
|
|
413
|
+
}
|
|
414
|
+
if (anyCapped)
|
|
415
|
+
return null;
|
|
416
|
+
return names.size === 1 ? [...names][0] : null;
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
331
422
|
/** Read `.planning/config.json` at `cwd`, distinguishing "file absent" from
|
|
332
423
|
* "file present but empty/malformed" — `INIT_IDENTITY_MISSING` only fires
|
|
333
424
|
* on the latter (see `verifyInitInvariants`'s own doc comment for why an
|
|
@@ -654,7 +745,7 @@ function computeCurrentStep(cwd, session, opts) {
|
|
|
654
745
|
// the convention: any failure anywhere in this body degrades to a
|
|
655
746
|
// `write_failed` verdict instead of propagating.
|
|
656
747
|
try {
|
|
657
|
-
const questions = buildInitQuestions(cwd);
|
|
748
|
+
const questions = buildInitQuestions(cwd, { _globalRoot: opts._globalRoot });
|
|
658
749
|
for (const q of questions) {
|
|
659
750
|
if (!Object.prototype.hasOwnProperty.call(session.answers, q.id)) {
|
|
660
751
|
return { kind: 'question', question: q };
|
|
@@ -168,7 +168,26 @@ async function runInitInteractive(cwd, prompter, opts = {}) {
|
|
|
168
168
|
preselectedLabel: state.preselectedLabel,
|
|
169
169
|
optionalAnswer: state.optionalAnswer,
|
|
170
170
|
});
|
|
171
|
-
|
|
171
|
+
// INIT-PATCH-C: Enter accepts a carried default, "none" clears it.
|
|
172
|
+
// Scoped to an OPTIONAL step carrying a usable `preselected` — today
|
|
173
|
+
// that is only `org_profile` when exactly one org-profile-shaped
|
|
174
|
+
// package was detected. `provider` also carries a `preselected` but is
|
|
175
|
+
// NOT `optionalAnswer`, so the `state.optionalAnswer === true` guard
|
|
176
|
+
// below leaves it untouched: a blank answer there still falls through
|
|
177
|
+
// to `initSessionAnswer` verbatim, which still rejects it and re-asks
|
|
178
|
+
// exactly as today.
|
|
179
|
+
const hasUsableDefault = state.optionalAnswer === true && typeof state.preselected === 'string' && state.preselected.trim() !== '';
|
|
180
|
+
let submittedAnswer = answer;
|
|
181
|
+
if (hasUsableDefault) {
|
|
182
|
+
const trimmedLower = answer.trim().toLowerCase();
|
|
183
|
+
if (answer.trim() === '') {
|
|
184
|
+
submittedAnswer = state.preselected;
|
|
185
|
+
}
|
|
186
|
+
else if (trimmedLower === 'none') {
|
|
187
|
+
submittedAnswer = '';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const result = (0, init_contract_cjs_1.initSessionAnswer)(cwd, stepId, submittedAnswer, sessionOpts);
|
|
172
191
|
if (result.ok) {
|
|
173
192
|
if (stepId === 'compose_confirm') {
|
|
174
193
|
wroteBinding = readBindingsContent(cwd) !== preBindings;
|
package/pp-core/bin/pp-tools.cjs
CHANGED
|
@@ -725,9 +725,17 @@ function createReadlinePrompter() {
|
|
|
725
725
|
)
|
|
726
726
|
.join(' / ');
|
|
727
727
|
rendered += `\n Options: ${optionsRendered}`;
|
|
728
|
+
} else if (question.preselected) {
|
|
729
|
+
// INIT-PATCH-C: an option-less question carrying a `preselected`
|
|
730
|
+
// (e.g. a detected org profile) rendered no default at all today —
|
|
731
|
+
// render one, naming it via `preselectedLabel` when present.
|
|
732
|
+
rendered += `\n Default: ${question.preselectedLabel || question.preselected} (press Enter to accept)`;
|
|
728
733
|
}
|
|
729
734
|
if (question.optionalAnswer) {
|
|
730
|
-
|
|
735
|
+
// The unconditional "Enter skips" hint would be a lie once a
|
|
736
|
+
// default is carried — Enter now ACCEPTS the default above, so the
|
|
737
|
+
// skip hint must point at "none" instead.
|
|
738
|
+
rendered += question.preselected ? '\n (type "none" to skip)' : '\n (press Enter to skip)';
|
|
731
739
|
}
|
|
732
740
|
return new Promise((resolve) => {
|
|
733
741
|
rl.question(`${rendered}\n> `, (answer) => resolve(answer));
|
|
@@ -1497,6 +1505,8 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
1497
1505
|
if (!process.stdin.isTTY) {
|
|
1498
1506
|
error(
|
|
1499
1507
|
'"pp-tools init" requires an interactive terminal (stdin is not a TTY) and will not prompt. ' +
|
|
1508
|
+
'This command cannot run inside an AI-agent shell or a piped session — open a regular ' +
|
|
1509
|
+
'terminal window (e.g. Terminal, iTerm, or Windows Terminal) and run it there. ' +
|
|
1500
1510
|
'For a non-interactive/CI-driven session, use "pp-tools query init-contract --session-next" / ' +
|
|
1501
1511
|
'"--session-answer <stepId>=<value>" directly instead.',
|
|
1502
1512
|
ERROR_REASON.INIT_NON_INTERACTIVE,
|