create-pathfinder 1.4.1 → 1.5.0
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/AGENTS.md +4 -0
- package/CLAUDE.md +7 -1
- package/README.md +97 -9
- package/bin/create-pathfinder.mjs +32 -4
- package/context/project-overview.md +14 -2
- package/copy-list.json +0 -1
- package/package.json +2 -2
- package/src/cli.mjs +765 -34
- package/src/clipboard.mjs +134 -0
- package/src/detect.mjs +183 -0
- package/src/editor.mjs +136 -0
- package/src/git.mjs +58 -0
- package/src/harnesses/adapter.mjs +288 -0
- package/src/harnesses/index.mjs +122 -0
- package/src/install.mjs +209 -1
- package/src/kickstart-prompt.mjs +81 -0
- package/src/prompt.mjs +307 -0
- package/templates/project-overview.template.md +14 -2
- package/prompts/01-kickstart-project.md +0 -1
- package/prompts/01-teach-current-feature.md +0 -9
- package/prompts/02-debate-me.md +0 -1
- package/prompts/02-quiz-current-feature.md +0 -7
- package/prompts/03-challenge-current-feature.md +0 -7
- package/prompts/03-prototype.md +0 -1
- package/prompts/04-teach-current-architecture.md +0 -7
- package/prompts/04-to-specs.md +0 -1
- package/prompts/05-learning-review.md +0 -5
- package/prompts/05-load-feature.md +0 -1
- package/prompts/06-start-feature.md +0 -1
- package/prompts/07-review-feature.md +0 -1
- package/prompts/08-complete-feature.md +0 -1
- package/prompts/09-learn-feature.md +0 -1
- package/prompts/10-learn-codebase.md +0 -1
- package/prompts/11-handoff.md +0 -1
- package/prompts/12-skillsmith.md +0 -1
- package/prompts/13-reverse-engineer.md +0 -18
- package/prompts/14-reflect.md +0 -13
- package/prompts/15-debug-issue.md +0 -15
package/src/cli.mjs
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Command-line surface: parse arguments, refuse unsafe situations, report.
|
|
3
3
|
*
|
|
4
|
-
* No CLI framework. The flag set is
|
|
5
|
-
* loop; a dependency tree for that would be indefensible in a project
|
|
6
|
-
* identity is "not a framework."
|
|
4
|
+
* No CLI framework. The flag set is a handful of booleans and the whole parser
|
|
5
|
+
* is a loop; a dependency tree for that would be indefensible in a project
|
|
6
|
+
* whose identity is "not a framework."
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import { applyPlan, planInstall } from "./install.mjs";
|
|
9
|
+
import { findKitRoot, COPY_LIST } from "./kit.mjs";
|
|
10
|
+
import { applyAdapterPlan, applyPlan, planAdapters, planInstall } from "./install.mjs";
|
|
11
|
+
import { detect, detectedToolLabels } from "./detect.mjs";
|
|
12
|
+
import { initRepository } from "./git.mjs";
|
|
13
|
+
import { nonInteractivePrompter } from "./prompt.mjs";
|
|
14
|
+
import { copyToClipboard } from "./clipboard.mjs";
|
|
15
|
+
import { detectEditors, openInEditor } from "./editor.mjs";
|
|
16
|
+
import { kickstartPrompt, kickstartPromptLines } from "./kickstart-prompt.mjs";
|
|
17
|
+
import {
|
|
18
|
+
HARNESSES,
|
|
19
|
+
HARNESS_IDS,
|
|
20
|
+
detectedHarnesses,
|
|
21
|
+
findHarness,
|
|
22
|
+
harnessNamed,
|
|
23
|
+
} from "./harnesses/index.mjs";
|
|
11
24
|
|
|
12
25
|
const USAGE = `Usage: npx create-pathfinder [options]
|
|
13
26
|
|
|
@@ -16,12 +29,45 @@ Installs the Pathfinder workflow kit into the current Git repository.
|
|
|
16
29
|
Copies: ${COPY_LIST.join(", ")}
|
|
17
30
|
|
|
18
31
|
Options:
|
|
19
|
-
--
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
--agents <ids> Generate skill adapters for these tools, comma-separated.
|
|
33
|
+
Valid ids: ${HARNESS_IDS.join(", ")}. Alias: --agent.
|
|
34
|
+
Without it, nothing is configured unless you are asked and
|
|
35
|
+
say so.
|
|
36
|
+
--dry-run Report what would be written, and any \`git init\` that would
|
|
37
|
+
run first; change nothing and ask nothing.
|
|
38
|
+
--force Overwrite files that already exist, and replace a file you
|
|
39
|
+
wrote at a path an adapter would occupy. Off by default.
|
|
40
|
+
--git-init Run \`git init\` here if this is not a repository yet.
|
|
41
|
+
--no-git-init Never run \`git init\`; refuse instead.
|
|
42
|
+
--no-clipboard Never offer to copy the Kickstart prompt. The prompt is
|
|
43
|
+
printed either way.
|
|
44
|
+
--no-open Never offer to open the project in an editor.
|
|
45
|
+
--yes Take the defaults and ask nothing. Alias: --no-input.
|
|
46
|
+
It does not authorize \`git init\` or configure any tool;
|
|
47
|
+
pass --git-init and --agents for those.
|
|
48
|
+
-h, --help Show this message.
|
|
49
|
+
|
|
50
|
+
Adapters are generated files Pathfinder owns and regenerates without --force.
|
|
51
|
+
A file it did not generate is never replaced, at any path, without --force.
|
|
52
|
+
|
|
53
|
+
Without a terminal on both stdin and stdout, nothing is ever asked. In that
|
|
54
|
+
case a directory that is not a Git repository needs --git-init, or the install
|
|
55
|
+
is refused, and neither your clipboard nor an editor is touched — --yes does
|
|
56
|
+
not authorize either one.
|
|
22
57
|
`;
|
|
23
58
|
|
|
24
|
-
export function run(
|
|
59
|
+
export async function run(
|
|
60
|
+
argv,
|
|
61
|
+
{
|
|
62
|
+
cwd,
|
|
63
|
+
out,
|
|
64
|
+
err,
|
|
65
|
+
env = {},
|
|
66
|
+
platform = process.platform,
|
|
67
|
+
stdoutIsTTY = false,
|
|
68
|
+
prompter = nonInteractivePrompter(),
|
|
69
|
+
},
|
|
70
|
+
) {
|
|
25
71
|
const options = parseArguments(argv);
|
|
26
72
|
|
|
27
73
|
if (options.error) {
|
|
@@ -34,18 +80,36 @@ export function run(argv, { cwd, out, err }) {
|
|
|
34
80
|
return 0;
|
|
35
81
|
}
|
|
36
82
|
|
|
83
|
+
// Detection runs before anything is decided and before anything is asked, so
|
|
84
|
+
// the user reads what the tool found before reading what it wants to do.
|
|
85
|
+
const findings = detect({ cwd, env, platform });
|
|
86
|
+
const unicode = supportsUnicode(env, platform);
|
|
87
|
+
const mark = marks(unicode);
|
|
88
|
+
|
|
89
|
+
// Printed only to a terminal. The report is for a person, and the acceptance
|
|
90
|
+
// criteria require non-interactive output to stay what 1.4.1 produced — so a
|
|
91
|
+
// piped run, a CI log, and `> install.txt` all keep the old bytes.
|
|
92
|
+
if (stdoutIsTTY) {
|
|
93
|
+
out(formatFindings(findings, { unicode }));
|
|
94
|
+
}
|
|
95
|
+
|
|
37
96
|
// Refused rather than allowed with a warning: this tool writes several
|
|
38
97
|
// hundred files, and without version control the user has no way to inspect
|
|
39
|
-
// or undo what it did.
|
|
40
|
-
|
|
98
|
+
// or undo what it did. What is new in this chunk is that the refusal is no
|
|
99
|
+
// longer the only outcome — the tool may offer to satisfy the requirement.
|
|
100
|
+
let gitRoot = findings.git.repositoryRoot;
|
|
101
|
+
let initializeGit = false;
|
|
102
|
+
|
|
41
103
|
if (gitRoot === null) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
104
|
+
const decision = await decideGitInit({ findings, options, prompter, cwd, out });
|
|
105
|
+
if (!decision.approved) {
|
|
106
|
+
err(decision.message);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
initializeGit = true;
|
|
110
|
+
// The repository we are about to create is this directory, so every check
|
|
111
|
+
// and every message downstream reads the same as if it had always been one.
|
|
112
|
+
gitRoot = cwd;
|
|
49
113
|
}
|
|
50
114
|
|
|
51
115
|
const kitRoot = findKitRoot();
|
|
@@ -67,17 +131,235 @@ export function run(argv, { cwd, out, err }) {
|
|
|
67
131
|
return 1;
|
|
68
132
|
}
|
|
69
133
|
|
|
134
|
+
// Deliberately after both kit checks. Approval to initialize is not approval
|
|
135
|
+
// to leave a `.git` behind for an install that was never going to happen:
|
|
136
|
+
// a broken package and the kit repository itself both bail out above, with
|
|
137
|
+
// the directory exactly as they found it.
|
|
138
|
+
if (initializeGit) {
|
|
139
|
+
if (options.dryRun) {
|
|
140
|
+
out(` ${mark.info} Would run \`git init\` in ${cwd}\n\n`);
|
|
141
|
+
} else {
|
|
142
|
+
const initialized = initRepository(cwd);
|
|
143
|
+
if (!initialized.ok) {
|
|
144
|
+
err(
|
|
145
|
+
"create-pathfinder: `git init` failed, so nothing was installed.\n\n" +
|
|
146
|
+
`${indent(initialized.message)}\n\n` +
|
|
147
|
+
"Fix that, or run `git init` yourself, then run this again.\n",
|
|
148
|
+
);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
out(` ${mark.ok} git init ${mark.dash} initialized an empty repository in ${cwd}\n\n`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Asked before anything is written, so every question this run has is behind
|
|
156
|
+
// the user before the first file moves. Detection supplies the default and
|
|
157
|
+
// nothing more: a tool being installed on this machine is not permission to
|
|
158
|
+
// write into the project on its behalf.
|
|
159
|
+
const { harnesses, customTools } = await selectHarnesses({ findings, options, prompter, out });
|
|
160
|
+
|
|
70
161
|
const plan = planInstall(kitRoot, cwd, { force: options.force });
|
|
71
162
|
const result = applyPlan(plan, { dryRun: options.dryRun });
|
|
72
163
|
|
|
73
|
-
|
|
74
|
-
|
|
164
|
+
// Deliberately after the copy. An adapter delegates to a canonical file, so
|
|
165
|
+
// generating one beside a copy that failed would point the user's tool at a
|
|
166
|
+
// file that is not there.
|
|
167
|
+
const adapters = generateAdapters({ harnesses, kitRoot, cwd, options, result });
|
|
168
|
+
|
|
169
|
+
report({ result, plan, adapters, harnesses, customTools, cwd, gitRoot, options, out, err });
|
|
170
|
+
|
|
171
|
+
// After the report, because the first offer is about the prompt the report
|
|
172
|
+
// just printed — and because a question above the summary would make the user
|
|
173
|
+
// answer before seeing what happened. Every run that gets this far printed a
|
|
174
|
+
// prompt, including one that wrote no files, so there is nothing to guard on.
|
|
175
|
+
await offerOnboardingActions({ harnesses, cwd, options, prompter, out, env, platform });
|
|
176
|
+
|
|
177
|
+
// Neither offer can change this. An install that wrote every file succeeded
|
|
178
|
+
// whether or not the machine has `pbcopy` or an editor on it.
|
|
179
|
+
return result.errors.length > 0 || adapters.result.errors.length > 0 ? 1 : 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The list's last entry, which is not a harness and never becomes one.
|
|
184
|
+
*
|
|
185
|
+
* A sentinel rather than a registry row, because everything about a harness —
|
|
186
|
+
* a path, a detection, a rendered file — is exactly what this option does not
|
|
187
|
+
* have. Putting it in `HARNESSES` would mean every loop that writes files
|
|
188
|
+
* needing to remember to skip it, and one that forgot would generate adapters
|
|
189
|
+
* into a directory named after a tool that cannot read them.
|
|
190
|
+
*/
|
|
191
|
+
const SOMETHING_ELSE = Object.freeze({ label: "Something else…" });
|
|
192
|
+
|
|
193
|
+
/** How many custom names one run will take before it stops asking. */
|
|
194
|
+
const CUSTOM_TOOL_LIMIT = 10;
|
|
195
|
+
|
|
196
|
+
/** What a tool may be called here: letters, digits, and the punctuation names use. */
|
|
197
|
+
const CUSTOM_TOOL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 ._+-]{0,39}$/;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Which harnesses this run configures, and which tools it was told about but
|
|
201
|
+
* cannot configure. Possibly neither, which is the default.
|
|
202
|
+
*
|
|
203
|
+
* Three ways to answer, in precedence order: `--agents` says it outright, an
|
|
204
|
+
* interactive terminal is asked, and everything else configures nothing. That
|
|
205
|
+
* last one is what keeps a scripted run byte-identical to 1.4.1 — a CI job that
|
|
206
|
+
* has always piped this command sees no new files and no new output.
|
|
207
|
+
*
|
|
208
|
+
* `--agents` cannot name an unsupported tool: an id the registry does not know
|
|
209
|
+
* exits 2 rather than being recorded, because a flag is how a script asks for
|
|
210
|
+
* files and there is no honest way to half-satisfy that. "Something else…"
|
|
211
|
+
* exists in the question, where a person is there to read the answer.
|
|
212
|
+
*
|
|
213
|
+
* An unanswered prompt is read as "none", the same conservative reading the
|
|
214
|
+
* Git question gives it.
|
|
215
|
+
*
|
|
216
|
+
* @returns {Promise<{harnesses: object[], customTools: string[]}>}
|
|
217
|
+
*/
|
|
218
|
+
async function selectHarnesses({ findings, options, prompter, out }) {
|
|
219
|
+
if (options.agents !== null) {
|
|
220
|
+
return { harnesses: options.agents.map((id) => findHarness(id)), customTools: [] };
|
|
221
|
+
}
|
|
222
|
+
if (!prompter.interactive || options.yes) return { harnesses: [], customTools: [] };
|
|
223
|
+
|
|
224
|
+
const detected = detectedHarnesses(findings);
|
|
225
|
+
const entries = [...HARNESSES, SOMETHING_ELSE];
|
|
226
|
+
const width = Math.max(...entries.map((entry) => entry.label.length));
|
|
227
|
+
|
|
228
|
+
const answer = await prompter.chooseMany("Configure Pathfinder for which tools?", {
|
|
229
|
+
options: entries.map((entry) => ({
|
|
230
|
+
value: entry,
|
|
231
|
+
label:
|
|
232
|
+
`${entry.label.padEnd(width)} -> ` +
|
|
233
|
+
// The path is shown so nobody has to check a box to find out what it
|
|
234
|
+
// writes. The last entry earns the same courtesy by admitting it
|
|
235
|
+
// writes nothing, in the column where every other row names a file.
|
|
236
|
+
(entry === SOMETHING_ELSE
|
|
237
|
+
? "nothing is generated"
|
|
238
|
+
: `${entry.skillsDir}/` + (detected.includes(entry) ? " (detected)" : "")),
|
|
239
|
+
})),
|
|
240
|
+
defaultSelection: detected,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
out("\n");
|
|
244
|
+
|
|
245
|
+
const chosen = answer ?? [];
|
|
246
|
+
const harnesses = chosen.filter((entry) => entry !== SOMETHING_ELSE);
|
|
247
|
+
const customTools = chosen.includes(SOMETHING_ELSE) ? await askCustomTools({ prompter, out }) : [];
|
|
248
|
+
|
|
249
|
+
return { harnesses, customTools };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Collect the names of tools Pathfinder does not support.
|
|
254
|
+
*
|
|
255
|
+
* Nothing is generated from these and nothing is stored — they exist only so
|
|
256
|
+
* the summary can name what it is declining to do. That is the whole feature:
|
|
257
|
+
* an answer a person can act on beats a directory full of files their tool
|
|
258
|
+
* will never read.
|
|
259
|
+
*
|
|
260
|
+
* The loop is bounded three ways: an empty line, an ended stream, and a hard
|
|
261
|
+
* ceiling. The ceiling is not a guess about how many tools anyone uses; it is
|
|
262
|
+
* there because a question that repeats itself is a question that can repeat
|
|
263
|
+
* itself forever on a stream that never closes.
|
|
264
|
+
*/
|
|
265
|
+
async function askCustomTools({ prompter, out }) {
|
|
266
|
+
out(
|
|
267
|
+
"Pathfinder generates adapters only for tools it can generate them for.\n" +
|
|
268
|
+
"Name the others and the summary will say what does work for them.\n\n",
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const names = [];
|
|
272
|
+
|
|
273
|
+
while (names.length < CUSTOM_TOOL_LIMIT) {
|
|
274
|
+
const answer = await prompter.text("Which tool? (Enter when done)");
|
|
275
|
+
|
|
276
|
+
// "" is done and null is nobody there. Both stop, and neither is an error.
|
|
277
|
+
if (answer === null || answer === "") break;
|
|
278
|
+
|
|
279
|
+
const supported = harnessNamed(answer);
|
|
280
|
+
if (supported !== null) {
|
|
281
|
+
out(
|
|
282
|
+
` ${supported.label} is supported — it is in the list above, and writes to\n` +
|
|
283
|
+
` ${supported.skillsDir}/. Choose it there, or pass --agents ${supported.id}.\n\n`,
|
|
284
|
+
);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!CUSTOM_TOOL_PATTERN.test(answer)) {
|
|
289
|
+
// Nothing is built from this name, so the risk is not injection but a
|
|
290
|
+
// summary that says something other than what was typed. A name this
|
|
291
|
+
// tool cannot print back faithfully is one it should not accept.
|
|
292
|
+
out(" Letters, digits, spaces, and . _ + - only, up to 40 characters.\n\n");
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (names.some((name) => name.toLowerCase() === answer.toLowerCase())) continue;
|
|
297
|
+
names.push(answer);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (names.length > 0) out("\n");
|
|
301
|
+
return names;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Generate the adapters for the selected harnesses, or explain why not.
|
|
306
|
+
*
|
|
307
|
+
* Returns the plan and the result together so the report can distinguish "no
|
|
308
|
+
* harness was chosen" from "a harness was chosen and produced nothing", which
|
|
309
|
+
* are the same zero and mean opposite things.
|
|
310
|
+
*/
|
|
311
|
+
function generateAdapters({ harnesses, kitRoot, cwd, options, result }) {
|
|
312
|
+
const none = { plan: [], result: applyAdapterPlan([]), blocked: false };
|
|
313
|
+
|
|
314
|
+
if (harnesses.length === 0) return none;
|
|
315
|
+
|
|
316
|
+
// The copy failed part-way. Reporting adapters as generated on top of that
|
|
317
|
+
// would be a success message about a broken install.
|
|
318
|
+
if (result.errors.length > 0) return { ...none, blocked: true };
|
|
319
|
+
|
|
320
|
+
const plan = planAdapters(harnesses, { kitRoot, targetRoot: cwd, force: options.force });
|
|
321
|
+
return { plan, result: applyAdapterPlan(plan, { dryRun: options.dryRun }), blocked: false };
|
|
75
322
|
}
|
|
76
323
|
|
|
77
324
|
function parseArguments(argv) {
|
|
78
|
-
const options = {
|
|
325
|
+
const options = {
|
|
326
|
+
dryRun: false,
|
|
327
|
+
force: false,
|
|
328
|
+
help: false,
|
|
329
|
+
gitInit: false,
|
|
330
|
+
noGitInit: false,
|
|
331
|
+
noClipboard: false,
|
|
332
|
+
noOpen: false,
|
|
333
|
+
yes: false,
|
|
334
|
+
// null means "not said", which is not the same as "none". Only the first
|
|
335
|
+
// suppresses the question.
|
|
336
|
+
agents: null,
|
|
337
|
+
error: null,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
341
|
+
const argument = argv[index];
|
|
342
|
+
|
|
343
|
+
const isAgents =
|
|
344
|
+
argument === "--agents" ||
|
|
345
|
+
argument === "--agent" ||
|
|
346
|
+
argument.startsWith("--agents=") ||
|
|
347
|
+
argument.startsWith("--agent=");
|
|
348
|
+
|
|
349
|
+
if (isAgents) {
|
|
350
|
+
const equals = argument.indexOf("=");
|
|
351
|
+
const value = equals === -1 ? argv[++index] : argument.slice(equals + 1);
|
|
352
|
+
const parsed = parseAgents(value);
|
|
353
|
+
|
|
354
|
+
if (parsed.error) {
|
|
355
|
+
options.error = parsed.error;
|
|
356
|
+
return options;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
options.agents = [...new Set([...(options.agents ?? []), ...parsed.ids])];
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
79
362
|
|
|
80
|
-
for (const argument of argv) {
|
|
81
363
|
switch (argument) {
|
|
82
364
|
case "--dry-run":
|
|
83
365
|
options.dryRun = true;
|
|
@@ -85,6 +367,26 @@ function parseArguments(argv) {
|
|
|
85
367
|
case "--force":
|
|
86
368
|
options.force = true;
|
|
87
369
|
break;
|
|
370
|
+
case "--git-init":
|
|
371
|
+
options.gitInit = true;
|
|
372
|
+
break;
|
|
373
|
+
case "--no-git-init":
|
|
374
|
+
options.noGitInit = true;
|
|
375
|
+
break;
|
|
376
|
+
case "--no-clipboard":
|
|
377
|
+
options.noClipboard = true;
|
|
378
|
+
break;
|
|
379
|
+
case "--no-open":
|
|
380
|
+
options.noOpen = true;
|
|
381
|
+
break;
|
|
382
|
+
// `--yes` silences questions. It does not answer the Git one: authorizing
|
|
383
|
+
// the creation of a repository is the single thing in this tool that has
|
|
384
|
+
// to be said out loud, and "assume yes to everything" is exactly the kind
|
|
385
|
+
// of blanket that should not cover it.
|
|
386
|
+
case "--yes":
|
|
387
|
+
case "--no-input":
|
|
388
|
+
options.yes = true;
|
|
389
|
+
break;
|
|
88
390
|
case "-h":
|
|
89
391
|
case "--help":
|
|
90
392
|
options.help = true;
|
|
@@ -95,9 +397,209 @@ function parseArguments(argv) {
|
|
|
95
397
|
}
|
|
96
398
|
}
|
|
97
399
|
|
|
400
|
+
if (options.gitInit && options.noGitInit) {
|
|
401
|
+
options.error = "`--git-init` and `--no-git-init` contradict each other";
|
|
402
|
+
}
|
|
403
|
+
|
|
98
404
|
return options;
|
|
99
405
|
}
|
|
100
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Read `--agents claude-code,codex`.
|
|
409
|
+
*
|
|
410
|
+
* An unknown id is a refusal, not a warning that drops it: someone who typed
|
|
411
|
+
* `--agents claud-code` wants adapters, and quietly installing none while
|
|
412
|
+
* exiting 0 would tell them it worked. The valid ids are named in the message,
|
|
413
|
+
* because the whole list is short enough to be the answer.
|
|
414
|
+
*/
|
|
415
|
+
function parseAgents(value) {
|
|
416
|
+
if (value === undefined || value.startsWith("-")) {
|
|
417
|
+
return { error: "`--agents` needs a value, such as `--agents " + HARNESS_IDS[0] + "`" };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const ids = value
|
|
421
|
+
.split(",")
|
|
422
|
+
.map((id) => id.trim())
|
|
423
|
+
.filter((id) => id !== "");
|
|
424
|
+
|
|
425
|
+
if (ids.length === 0) {
|
|
426
|
+
return { error: "`--agents` needs a value, such as `--agents " + HARNESS_IDS[0] + "`" };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const unknown = ids.find((id) => findHarness(id) === null);
|
|
430
|
+
if (unknown !== undefined) {
|
|
431
|
+
return { error: `unknown agent \`${unknown}\`. Valid ids: ${HARNESS_IDS.join(", ")}` };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return { ids };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* May this directory become a repository?
|
|
439
|
+
*
|
|
440
|
+
* The four ways to reach "no" are kept apart because they are four different
|
|
441
|
+
* situations for the person reading the message: they said no, they said never,
|
|
442
|
+
* nobody was there to ask, or the machine cannot do it at all. Only the last of
|
|
443
|
+
* those makes `--git-init` bad advice, which is why it is checked before the
|
|
444
|
+
* flags — offering a flag that cannot work would be worse than the refusal it
|
|
445
|
+
* replaced.
|
|
446
|
+
*
|
|
447
|
+
* Asks nothing unless the answer is genuinely unknown *and* someone is there to
|
|
448
|
+
* answer. Returns rather than exits: the caller owns the exit code.
|
|
449
|
+
*
|
|
450
|
+
* @returns {{approved: true} | {approved: false, message: string}}
|
|
451
|
+
*/
|
|
452
|
+
async function decideGitInit({ findings, options, prompter, cwd, out }) {
|
|
453
|
+
if (!findings.git.binary) {
|
|
454
|
+
return {
|
|
455
|
+
approved: false,
|
|
456
|
+
message:
|
|
457
|
+
`create-pathfinder: ${cwd} is not inside a Git repository, and \`git\`\n` +
|
|
458
|
+
"is not available to create one.\n\n" +
|
|
459
|
+
"The kit is installed into version control so you can review the files\n" +
|
|
460
|
+
"it adds and undo them if you change your mind. Install Git\n" +
|
|
461
|
+
"(https://git-scm.com/downloads), or cd into an existing repository,\n" +
|
|
462
|
+
"then run this again.\n",
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (options.noGitInit) return { approved: false, message: refusal(cwd) };
|
|
467
|
+
if (options.gitInit) return { approved: true };
|
|
468
|
+
|
|
469
|
+
// `--dry-run` needs no permission, because there is nothing to permit. The
|
|
470
|
+
// spec forbids asking a question whose only purpose is to authorize an action
|
|
471
|
+
// that will not be taken, and this is that question: the file plan is the
|
|
472
|
+
// same whatever the answer, so the prompt would buy a report the tool could
|
|
473
|
+
// have written anyway. Reporting the `git init` it *would* run is the whole
|
|
474
|
+
// point of the mode — telling someone "no" about work nobody was going to do
|
|
475
|
+
// withholds the one thing they asked for.
|
|
476
|
+
if (options.dryRun) return { approved: true };
|
|
477
|
+
|
|
478
|
+
// The TTY guard. Both ends must be a terminal, and `--yes` opts out on the
|
|
479
|
+
// user's behalf. Below this line the tool is scriptable: it asks nothing,
|
|
480
|
+
// prints no question, and refuses the way 1.4.1 did.
|
|
481
|
+
if (!prompter.interactive || options.yes) return { approved: false, message: refusal(cwd) };
|
|
482
|
+
|
|
483
|
+
out(
|
|
484
|
+
"Pathfinder installs into version control so you can review what it wrote\n" +
|
|
485
|
+
"and undo it. It will not touch an existing history.\n\n",
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
const answer = await prompter.confirm("Initialize a Git repository here?", {
|
|
489
|
+
defaultAnswer: true,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (answer === true) return { approved: true };
|
|
493
|
+
|
|
494
|
+
// `false` is a decision and `null` is an unanswerable prompt — a closed
|
|
495
|
+
// stdin, or input that never resolved to a yes or a no. Both land here,
|
|
496
|
+
// because the only safe reading of "no usable approval" is that there is no
|
|
497
|
+
// approval. Declining is not an error, but it is still a refusal to install.
|
|
498
|
+
return {
|
|
499
|
+
approved: false,
|
|
500
|
+
message:
|
|
501
|
+
"\nNothing was installed.\n\n" +
|
|
502
|
+
"Pathfinder writes the whole kit into your project. Without version\n" +
|
|
503
|
+
"control there is no way to review or undo that, so it will not run\n" +
|
|
504
|
+
"outside a repository.\n\n" +
|
|
505
|
+
"Run `git init` here yourself and try again, or cd into an existing\n" +
|
|
506
|
+
"repository.\n",
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* The 1.4.1 refusal, plus the one sentence this feature earns the right to add.
|
|
512
|
+
*
|
|
513
|
+
* Byte-identical to what `1.4.1` printed through the final line, so every
|
|
514
|
+
* non-interactive scenario that existed before this feature still reads the
|
|
515
|
+
* same; the flag is named after it rather than woven into it.
|
|
516
|
+
*/
|
|
517
|
+
function refusal(cwd) {
|
|
518
|
+
return (
|
|
519
|
+
`create-pathfinder: ${cwd} is not inside a Git repository.\n\n` +
|
|
520
|
+
"The kit is installed into version control so you can review the\n" +
|
|
521
|
+
"files it adds and undo them if you change your mind. Run `git init`\n" +
|
|
522
|
+
"here first, or cd into an existing repository, then run this again.\n" +
|
|
523
|
+
"\nTo have this command run `git init` for you, pass --git-init.\n"
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Prefix every line, so borrowed output is visibly not ours. */
|
|
528
|
+
function indent(text) {
|
|
529
|
+
return text
|
|
530
|
+
.split("\n")
|
|
531
|
+
.map((line) => ` ${line}`)
|
|
532
|
+
.join("\n");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Say what was found, before saying what will be done.
|
|
537
|
+
*
|
|
538
|
+
* Deliberately short, and deliberately passive. Every line states a fact about
|
|
539
|
+
* the machine; none of them implies an intention. The parenthetical on the
|
|
540
|
+
* tools line is load-bearing — a bare list of everything installed on someone's
|
|
541
|
+
* laptop reads like an announcement that all of it is about to be configured,
|
|
542
|
+
* which is not true here and will still not be true after Feature 11, where
|
|
543
|
+
* configuring anything requires an answer to a question.
|
|
544
|
+
*/
|
|
545
|
+
export function formatFindings(findings, { unicode = false } = {}) {
|
|
546
|
+
const mark = marks(unicode);
|
|
547
|
+
|
|
548
|
+
const lines = ["", "Pathfinder", ""];
|
|
549
|
+
|
|
550
|
+
if (findings.git.insideRepository) {
|
|
551
|
+
lines.push(` ${mark.ok} Git repository detected`);
|
|
552
|
+
} else if (findings.git.binary) {
|
|
553
|
+
lines.push(` ${mark.info} No Git repository here`);
|
|
554
|
+
} else {
|
|
555
|
+
lines.push(` ${mark.bad} No Git repository here, and \`git\` is not on your PATH`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (findings.pathfinder.installed) {
|
|
559
|
+
const { skillCount } = findings.pathfinder;
|
|
560
|
+
lines.push(` ${mark.ok} Pathfinder already installed (${skillCount} skill${plural(skillCount)})`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const tools = detectedToolLabels(findings);
|
|
564
|
+
lines.push(
|
|
565
|
+
tools.length > 0
|
|
566
|
+
? ` ${mark.ok} Tools detected: ${tools.join(", ")} (noted, not configured)`
|
|
567
|
+
: ` ${mark.info} No supported tools detected`,
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
// Trailing blank line: whatever comes next is a different statement — the
|
|
571
|
+
// install summary, a refusal, or in a later chunk a question — and it must
|
|
572
|
+
// not read as a sixth finding.
|
|
573
|
+
return lines.join("\n") + "\n\n";
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* The line markers, in whichever alphabet this terminal can be trusted with.
|
|
578
|
+
*
|
|
579
|
+
* One table, so a finding and an action it leads to are marked the same way.
|
|
580
|
+
*/
|
|
581
|
+
function marks(unicode) {
|
|
582
|
+
return unicode
|
|
583
|
+
? { ok: "✓", info: "·", bad: "✗", dash: "—" }
|
|
584
|
+
: { ok: "+", info: "-", bad: "!", dash: "-" };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Can this terminal be trusted with the decorated marks?
|
|
589
|
+
*
|
|
590
|
+
* Answered from the environment rather than attempted and hoped for, and biased
|
|
591
|
+
* hard toward "no": an unanswerable environment gets ASCII, which is readable
|
|
592
|
+
* everywhere, while a wrong "yes" leaves mojibake in the first output a new
|
|
593
|
+
* user ever sees from this tool.
|
|
594
|
+
*/
|
|
595
|
+
function supportsUnicode(env, platform) {
|
|
596
|
+
if (platform === "win32") {
|
|
597
|
+
return Boolean(env.WT_SESSION) || env.TERM_PROGRAM === "vscode";
|
|
598
|
+
}
|
|
599
|
+
const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
|
|
600
|
+
return /utf-?8/i.test(locale);
|
|
601
|
+
}
|
|
602
|
+
|
|
101
603
|
/**
|
|
102
604
|
* Say what happened, in full.
|
|
103
605
|
*
|
|
@@ -105,7 +607,7 @@ function parseArguments(argv) {
|
|
|
105
607
|
* default mode is that it left your work alone, and a bare "42 skipped" does
|
|
106
608
|
* not let anyone check that claim.
|
|
107
609
|
*/
|
|
108
|
-
function report({ result, plan, cwd, gitRoot, options, out, err }) {
|
|
610
|
+
function report({ result, plan, adapters, harnesses, customTools, cwd, gitRoot, options, out, err }) {
|
|
109
611
|
const lines = [];
|
|
110
612
|
const verb = options.dryRun ? "Would install" : "Installed";
|
|
111
613
|
|
|
@@ -122,6 +624,9 @@ function report({ result, plan, cwd, gitRoot, options, out, err }) {
|
|
|
122
624
|
lines.push(` ${result.overwritten} file${plural(result.overwritten)} overwritten (--force)`);
|
|
123
625
|
}
|
|
124
626
|
|
|
627
|
+
lines.push(...adapterLines({ adapters, harnesses, options }));
|
|
628
|
+
lines.push(...customToolLines(customTools));
|
|
629
|
+
|
|
125
630
|
const skipped = plan.filter((item) => item.status === "skip");
|
|
126
631
|
if (skipped.length > 0) {
|
|
127
632
|
lines.push(` ${skipped.length} file${plural(skipped.length)} left untouched because they already exist:`);
|
|
@@ -133,24 +638,250 @@ function report({ result, plan, cwd, gitRoot, options, out, err }) {
|
|
|
133
638
|
if (written === 0 && skipped.length === plan.length) {
|
|
134
639
|
lines.push("");
|
|
135
640
|
lines.push("The kit is already installed here.");
|
|
136
|
-
} else {
|
|
137
|
-
lines.push("");
|
|
138
|
-
lines.push("Next step — give your agent this prompt:");
|
|
139
|
-
lines.push("");
|
|
140
|
-
lines.push(" Use skills/kickstart-pathfinder/SKILL.md. Help me initialize this");
|
|
141
|
-
lines.push(" project. Do not install packages or write product code yet.");
|
|
142
641
|
}
|
|
143
642
|
|
|
643
|
+
// The prompt is printed here, always — including on a re-run that wrote
|
|
644
|
+
// nothing. A second run is how someone configures a harness they skipped, or
|
|
645
|
+
// simply comes back for the invocation they have forgotten, and both of those
|
|
646
|
+
// want the same line. It is also what makes the clipboard a convenience on
|
|
647
|
+
// top of this block rather than the only channel, which is what lets every
|
|
648
|
+
// clipboard failure be a non-event.
|
|
649
|
+
lines.push("");
|
|
650
|
+
lines.push("Next step — give your agent this prompt:");
|
|
651
|
+
lines.push("");
|
|
652
|
+
lines.push(...kickstartPromptLines(harnesses));
|
|
653
|
+
|
|
144
654
|
out(lines.join("\n") + "\n");
|
|
145
655
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
err(`\ncreate-pathfinder: ${result.errors.length} file${plural(result.errors.length)} could not be written:\n${failures}\n`);
|
|
656
|
+
const failures = [...result.errors, ...adapters.result.errors];
|
|
657
|
+
if (failures.length > 0) {
|
|
658
|
+
const detail = failures.map((error) => ` ${error.relativePath}: ${error.message}`).join("\n");
|
|
659
|
+
err(`\ncreate-pathfinder: ${failures.length} file${plural(failures.length)} could not be written:\n${detail}\n`);
|
|
151
660
|
}
|
|
152
661
|
}
|
|
153
662
|
|
|
663
|
+
/**
|
|
664
|
+
* The last two lines of a successful install: the clipboard, then the editor.
|
|
665
|
+
*
|
|
666
|
+
* The guards they share live here because they are one rule, not two. Both are
|
|
667
|
+
* skipped without a terminal because nothing may be asked there; both are
|
|
668
|
+
* skipped under `--yes` because silence is not consent to overwrite what
|
|
669
|
+
* someone is carrying around in their clipboard, nor to take over their screen;
|
|
670
|
+
* and both are skipped under `--dry-run` because a mode whose promise is
|
|
671
|
+
* "changes nothing" cannot make an exception for the state that lives outside
|
|
672
|
+
* the directory.
|
|
673
|
+
*
|
|
674
|
+
* Returns nothing and reports nothing upward on purpose. There is no outcome
|
|
675
|
+
* here that an install should be judged by, so there is no value for the exit
|
|
676
|
+
* code to be computed from.
|
|
677
|
+
*/
|
|
678
|
+
async function offerOnboardingActions({ harnesses, cwd, options, prompter, out, env, platform }) {
|
|
679
|
+
if (!prompter.interactive || options.yes) return;
|
|
680
|
+
|
|
681
|
+
const suppressed = options.noClipboard && options.noOpen;
|
|
682
|
+
|
|
683
|
+
if (options.dryRun) {
|
|
684
|
+
// Said rather than silently skipped, because the flag's whole job is to
|
|
685
|
+
// describe the run — and "it would have asked about your clipboard" is
|
|
686
|
+
// exactly the kind of thing someone runs a dry run to find out. Nothing is
|
|
687
|
+
// said when both flags already ruled both offers out; there is no run being
|
|
688
|
+
// described at that point.
|
|
689
|
+
if (!suppressed) {
|
|
690
|
+
out("\nOnboarding actions are not offered in a dry run; nothing was copied or opened.\n");
|
|
691
|
+
}
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (!options.noClipboard) await offerClipboard({ harnesses, options, prompter, out, env, platform });
|
|
696
|
+
if (!options.noOpen) await offerEditor({ cwd, prompter, out, env, platform });
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Offer to put the printed prompt on the clipboard. Never take it.
|
|
701
|
+
*/
|
|
702
|
+
async function offerClipboard({ harnesses, options, prompter, out, env, platform }) {
|
|
703
|
+
const answer = await prompter.confirm(
|
|
704
|
+
"Copy that prompt to your clipboard? This replaces what is on it now.",
|
|
705
|
+
{ defaultAnswer: true },
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
// `false` is a decline and `null` is nobody there. Neither is a yes, and only
|
|
709
|
+
// a yes may touch the clipboard.
|
|
710
|
+
if (answer !== true) return;
|
|
711
|
+
|
|
712
|
+
// No trailing newline, deliberately. Both harnesses treat a pasted newline as
|
|
713
|
+
// Enter, so appending one would submit the prompt the instant it is pasted —
|
|
714
|
+
// a surprise, not a convenience, and the opposite of leaving the user in
|
|
715
|
+
// control of when their session starts.
|
|
716
|
+
const copied = copyToClipboard(kickstartPrompt(harnesses), { env, platform });
|
|
717
|
+
|
|
718
|
+
out(
|
|
719
|
+
copied.ok
|
|
720
|
+
? " Copied.\n"
|
|
721
|
+
: ` Not copied — ${copied.reason}. The prompt is printed above.\n`,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Offer to open the project in an editor that is already installed here.
|
|
727
|
+
*
|
|
728
|
+
* The question exists only when there is something to answer it with. No editor
|
|
729
|
+
* on PATH means no question at all, rather than a question whose honest answer
|
|
730
|
+
* is "then don't" — an installer that asks about software you do not have is
|
|
731
|
+
* asking to be told about itself.
|
|
732
|
+
*
|
|
733
|
+
* One editor is a yes/no; several are a numbered list with an explicit way out.
|
|
734
|
+
* Neither shape can be answered by not answering: a decline, an unanswered
|
|
735
|
+
* question, and "Don't open" all land on the same nothing.
|
|
736
|
+
*/
|
|
737
|
+
async function offerEditor({ cwd, prompter, out, env, platform }) {
|
|
738
|
+
const editors = detectEditors({ env, platform });
|
|
739
|
+
if (editors.length === 0) return;
|
|
740
|
+
|
|
741
|
+
const chosen =
|
|
742
|
+
editors.length === 1
|
|
743
|
+
? (await prompter.confirm(`Open this project in ${editors[0].label}?`, {
|
|
744
|
+
defaultAnswer: true,
|
|
745
|
+
})) === true
|
|
746
|
+
? editors[0]
|
|
747
|
+
: null
|
|
748
|
+
: await prompter.chooseOne("Open this project in an editor?", {
|
|
749
|
+
options: [
|
|
750
|
+
...editors.map((editor) => ({ value: editor, label: editor.label })),
|
|
751
|
+
// Last, and a real row rather than a rule about the empty answer,
|
|
752
|
+
// so declining costs the same one keystroke as accepting.
|
|
753
|
+
{ value: null, label: "Don't open" },
|
|
754
|
+
],
|
|
755
|
+
defaultValue: editors[0],
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
if (chosen === null) return;
|
|
759
|
+
|
|
760
|
+
const opened = await openInEditor(chosen, cwd, { env, platform });
|
|
761
|
+
|
|
762
|
+
// "Opening", not "Opened". The launch is detached, so what this line can
|
|
763
|
+
// truthfully report is that the editor was started, not that it has drawn a
|
|
764
|
+
// window — and a failure after that point is the editor's to explain.
|
|
765
|
+
out(
|
|
766
|
+
opened.ok
|
|
767
|
+
? ` Opening ${chosen.label}.\n`
|
|
768
|
+
: ` Not opened — ${opened.reason}. The install is complete; open ${cwd} yourself.\n`,
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* The adapter half of the summary.
|
|
774
|
+
*
|
|
775
|
+
* Three counts, kept apart because they answer three different worries: what
|
|
776
|
+
* was generated, what was already right, and what was left alone. The third is
|
|
777
|
+
* the one that matters to someone re-running this over a project they have
|
|
778
|
+
* worked in, so conflicts are listed by name — a bare count would ask them to
|
|
779
|
+
* take it on faith that their file survived.
|
|
780
|
+
*
|
|
781
|
+
* Empty when no harness was chosen, which is the default and must stay
|
|
782
|
+
* invisible: a scripted 1.4.1-era run prints exactly what it always did.
|
|
783
|
+
*/
|
|
784
|
+
function adapterLines({ adapters, harnesses, options }) {
|
|
785
|
+
if (harnesses.length === 0) return [];
|
|
786
|
+
|
|
787
|
+
if (adapters.blocked) {
|
|
788
|
+
return [
|
|
789
|
+
"",
|
|
790
|
+
" No adapters were generated, because the kit copy did not finish.",
|
|
791
|
+
" An adapter delegates to a canonical skill file, and pointing your tool",
|
|
792
|
+
" at a file that was not written would be worse than generating nothing.",
|
|
793
|
+
];
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const failed = new Set(adapters.result.errors.map((error) => error.relativePath));
|
|
797
|
+
const lines = [];
|
|
798
|
+
|
|
799
|
+
for (const harness of harnesses) {
|
|
800
|
+
const mine = adapters.plan.filter(
|
|
801
|
+
(item) => item.harness === harness && !failed.has(item.relativePath),
|
|
802
|
+
);
|
|
803
|
+
const count = (action) => mine.filter((item) => item.action === action).length;
|
|
804
|
+
|
|
805
|
+
const generated = count("write");
|
|
806
|
+
const replaced = count("replace");
|
|
807
|
+
const unchanged = count("up-to-date");
|
|
808
|
+
const conflicts = mine.filter((item) => item.action === "conflict");
|
|
809
|
+
const orphans = mine.filter((item) => item.action === "orphan");
|
|
810
|
+
|
|
811
|
+
lines.push(
|
|
812
|
+
` ${generated} ${harness.label} skill adapter${plural(generated)} ` +
|
|
813
|
+
(options.dryRun ? "to generate" : "generated"),
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
if (replaced > 0) {
|
|
817
|
+
lines.push(` ${replaced} ${harness.label} adapter${plural(replaced)} replaced (--force)`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (unchanged > 0) {
|
|
821
|
+
lines.push(` ${unchanged} ${harness.label} adapter${plural(unchanged)} already up to date`);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (conflicts.length > 0) {
|
|
825
|
+
lines.push(
|
|
826
|
+
` ${conflicts.length} file${plural(conflicts.length)} left untouched because Pathfinder did not write ${conflicts.length === 1 ? "it" : "them"}:`,
|
|
827
|
+
);
|
|
828
|
+
for (const item of conflicts) lines.push(` ${item.relativePath}`);
|
|
829
|
+
lines.push("");
|
|
830
|
+
lines.push(
|
|
831
|
+
conflicts.length === 1
|
|
832
|
+
? " Re-run with --force to replace it — note that --force also overwrites"
|
|
833
|
+
: " Re-run with --force to replace them — note that --force also overwrites",
|
|
834
|
+
);
|
|
835
|
+
lines.push(" Pathfinder kit files you have edited.",
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
for (const item of orphans) {
|
|
840
|
+
lines.push(` ${item.relativePath} delegates to a skill this version no longer`);
|
|
841
|
+
lines.push(" ships. It was left in place; delete it yourself if you want it gone.");
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return lines;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* The tools this run was told about and cannot configure.
|
|
850
|
+
*
|
|
851
|
+
* The point of the whole option, and the reason it is worth a prompt: it is an
|
|
852
|
+
* answer rather than a gap. Pathfinder could plausibly write `.mdc` files for
|
|
853
|
+
* Cursor or drop a `SKILL.md` into any directory a tool might one day read, and
|
|
854
|
+
* every one of those would be a file the user's tool ignores while their
|
|
855
|
+
* installer summary claims success.
|
|
856
|
+
*
|
|
857
|
+
* So this states three things and stops: what does not exist, and the two
|
|
858
|
+
* things that already work. No apology, because nothing here went wrong, and
|
|
859
|
+
* no "yet", "planned", or "for now", because a summary is not the place to
|
|
860
|
+
* imply a roadmap nobody has committed to.
|
|
861
|
+
*/
|
|
862
|
+
function customToolLines(customTools = []) {
|
|
863
|
+
if (customTools.length === 0) return [];
|
|
864
|
+
|
|
865
|
+
return [
|
|
866
|
+
"",
|
|
867
|
+
` Pathfinder has no native integration for ${joinNames(customTools)}, so`,
|
|
868
|
+
` nothing is generated for ${customTools.length === 1 ? "it" : "them"}. Two things already work:`,
|
|
869
|
+
"",
|
|
870
|
+
" - The kit installs AGENTS.md at the repository root, which Codex,",
|
|
871
|
+
" Cursor, and several other tools read.",
|
|
872
|
+
" - Any agent can be given the line the adapters delegate to anyway:",
|
|
873
|
+
"",
|
|
874
|
+
" Use skills/<name>/SKILL.md and follow it exactly.",
|
|
875
|
+
];
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** `a`, `a and b`, `a, b, and c`. */
|
|
879
|
+
function joinNames(names) {
|
|
880
|
+
if (names.length === 1) return names[0];
|
|
881
|
+
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
882
|
+
return `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`;
|
|
883
|
+
}
|
|
884
|
+
|
|
154
885
|
function plural(count) {
|
|
155
886
|
return count === 1 ? "" : "s";
|
|
156
887
|
}
|