carrick 0.3.92 → 0.3.93
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/README.md +2 -1
- package/dist/auth/oauth.d.ts +8 -0
- package/dist/auth/oauth.js +9 -1
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/read.d.ts +5 -0
- package/dist/auth/read.js +11 -1
- package/dist/auth/read.js.map +1 -1
- package/dist/auth/run.d.ts +10 -0
- package/dist/auth/run.js +16 -0
- package/dist/auth/run.js.map +1 -1
- package/dist/init/connect.d.ts +19 -7
- package/dist/init/connect.js +29 -16
- package/dist/init/connect.js.map +1 -1
- package/dist/init/projects.d.ts +16 -3
- package/dist/init/projects.js +27 -18
- package/dist/init/projects.js.map +1 -1
- package/dist/init/remove.js +28 -5
- package/dist/init/remove.js.map +1 -1
- package/dist/init/repo-copies.d.ts +56 -0
- package/dist/init/repo-copies.js +241 -0
- package/dist/init/repo-copies.js.map +1 -0
- package/dist/init/repos.d.ts +27 -0
- package/dist/init/repos.js +67 -0
- package/dist/init/repos.js.map +1 -1
- package/dist/init/run.d.ts +109 -48
- package/dist/init/run.js +331 -154
- package/dist/init/run.js.map +1 -1
- package/dist/init/task-skills.d.ts +6 -6
- package/dist/init/task-skills.js +9 -9
- package/dist/init/task-skills.js.map +1 -1
- package/package.json +6 -6
- package/sidecar/dist/src/capture/check-probe.d.ts +6 -1
- package/sidecar/dist/src/capture/check-probe.js +7 -2
- package/sidecar/dist/src/capture/index.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +1 -0
- package/sidecar/dist/src/index.js +8 -2
- package/sidecar/dist/src/retype.d.ts +41 -4
- package/sidecar/dist/src/retype.js +82 -8
package/dist/init/run.js
CHANGED
|
@@ -14,14 +14,15 @@ import { readCredential } from "../auth/credentials.js";
|
|
|
14
14
|
import { openBrowser } from "../auth/oauth.js";
|
|
15
15
|
import { signIn } from "../auth/run.js";
|
|
16
16
|
import { resolveRepos } from "../auth/read.js";
|
|
17
|
-
import { deriveWorkspace, selectedProposal, selectRepos, writeProposal, PROPOSAL_FILE, repoIdentity, } from "./repos.js";
|
|
17
|
+
import { deriveWorkspace, selectedProposal, selectRepos, siblingRepos, initFolderAbove, writeProposal, realPath, repoRoots, MAX_SIBLINGS, PROPOSAL_FILE, repoIdentity, } from "./repos.js";
|
|
18
18
|
import { connectLine, connectRepos, projectAssignments, reposAreInProject, unconnectedRepos, workspaceUrls, } from "./connect.js";
|
|
19
19
|
import { downloadHostedIndex, hostedReport, nativeRunner, STEP_LABEL } from "./hosted.js";
|
|
20
|
-
import { createProject, listProjects, planProject, projectLabel, projectStep, SLUG, } from "./projects.js";
|
|
20
|
+
import { createProject, listProjects, planProject, projectLabel, projectStep, reposPhrase, SLUG, } from "./projects.js";
|
|
21
|
+
import { writeRepoCopy } from "./repo-copies.js";
|
|
21
22
|
import { claudeCodeFound, connectMcpClients, mcpLine, offeredFileClients } from "./mcp.js";
|
|
22
23
|
import { hookCommand, mergeCarrickHooks, removeCarrickHooks } from "./settings.js";
|
|
23
24
|
import { CODEX_HOOKS_FILE, writeCodexHooks } from "./codex.js";
|
|
24
|
-
import { ignoredSkillRoots,
|
|
25
|
+
import { ignoredSkillRoots, taskSkillWarnings, writeTaskSkills } from "./task-skills.js";
|
|
25
26
|
import { recordCliVersion } from "./outdated.js";
|
|
26
27
|
import { findCarrick, offerGlobalInstall } from "../global-install.js";
|
|
27
28
|
import { currentVersion } from "../update.js";
|
|
@@ -62,6 +63,15 @@ export function scaffoldSentence(repos) {
|
|
|
62
63
|
const shown = names.length > 3 ? `${names.slice(0, 3).join(", ")} and ${names.length - 3} more` : names.join(", ");
|
|
63
64
|
return `Run the carrick scaffold tool for ${shown}, once each, passing that repo's owner/repo as \`repo\`, and follow what it returns.`;
|
|
64
65
|
}
|
|
66
|
+
/** The title of the block a run closes on when there is scaffolding left to do. */
|
|
67
|
+
export const NEXT_TITLE = "Next: paste this into a new agent session";
|
|
68
|
+
/**
|
|
69
|
+
* The line that tells the agent where the proposal is, for a run that set up
|
|
70
|
+
* the folder above the one it started in (carrick#1512).
|
|
71
|
+
*/
|
|
72
|
+
export function initFolderSentence(workspace) {
|
|
73
|
+
return `The init folder is ${tilde(workspace)}.`;
|
|
74
|
+
}
|
|
65
75
|
/** What a run with nothing left to set up closes on: what this machine can do now. */
|
|
66
76
|
export const READY_SENTENCE = "The index is on this machine: `carrick check <file>` answers for a file, and your agent can ask who calls an operation.";
|
|
67
77
|
const OWNER_REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
@@ -200,7 +210,7 @@ function carrickOnPath() {
|
|
|
200
210
|
return findCarrick() !== null;
|
|
201
211
|
}
|
|
202
212
|
/**
|
|
203
|
-
* The repos a project holds that this
|
|
213
|
+
* The repos a project holds that this machine does not, one line per project.
|
|
204
214
|
*
|
|
205
215
|
* Carrick answers across every repo in a project, so a machine holding half of
|
|
206
216
|
* one gets half the answers, and the connection line above names only what is
|
|
@@ -208,9 +218,13 @@ function carrickOnPath() {
|
|
|
208
218
|
* membership of every project the requested repos are in (carrick#993 row 18).
|
|
209
219
|
* Capped and counted like the identity lines, because a project can hold two
|
|
210
220
|
* hundred.
|
|
221
|
+
*
|
|
222
|
+
* `onMachine` is every GitHub repo found on this machine, not only the ones
|
|
223
|
+
* this run set up: a repo in the folder beside this one is on the machine, and
|
|
224
|
+
* saying otherwise was the false line in carrick#1512.
|
|
211
225
|
*/
|
|
212
|
-
export function absentRepos(identity,
|
|
213
|
-
const onDisk = new Set(
|
|
226
|
+
export function absentRepos(identity, onMachine, project, label = (slug) => slug) {
|
|
227
|
+
const onDisk = new Set(onMachine.map((name) => name.toLowerCase()));
|
|
214
228
|
const sole = identity.project_repos.length === 1;
|
|
215
229
|
const lines = [];
|
|
216
230
|
for (const entry of identity.project_repos) {
|
|
@@ -239,23 +253,20 @@ function listed(items, cap) {
|
|
|
239
253
|
return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
|
|
240
254
|
}
|
|
241
255
|
/**
|
|
242
|
-
* The one line that says what is set up
|
|
256
|
+
* The one line that says what is set up: the project and the repos in it
|
|
257
|
+
* from this run (carrick#1489, carrick#1512).
|
|
243
258
|
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
259
|
+
* The line David's mock settled on. What was written was the list the reader
|
|
260
|
+
* said yes to, and a hook file that could not be written has its own line, so
|
|
261
|
+
* what is left to say is where the repos are. Only repos with a GitHub remote
|
|
262
|
+
* are named: a repo with none is in no project.
|
|
248
263
|
*/
|
|
249
|
-
export function
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
...(state.mcp.length > 0 ? [`MCP in ${state.mcp.join(", ")}`] : []),
|
|
256
|
-
];
|
|
257
|
-
const head = state.repos.length === 0 ? `Set up in ${where}` : `Set up ${subject} in ${where}`;
|
|
258
|
-
return parts.length === 0 ? head : `${head}: ${parts.join(", ")}`;
|
|
264
|
+
export function summaryLine(project, repos) {
|
|
265
|
+
const sorted = [...repos].sort();
|
|
266
|
+
const names = sorted.length > 10 ? `${sorted.slice(0, 10).join(", ")} and ${sorted.length - 10} more` : sorted.join(", ");
|
|
267
|
+
if (project === null)
|
|
268
|
+
return repos.length === 0 ? "No project" : `No project: ${names}`;
|
|
269
|
+
return repos.length === 0 ? project : `${project}: ${names}`;
|
|
259
270
|
}
|
|
260
271
|
/**
|
|
261
272
|
* The installs a first scan would be refused over, as commands to run.
|
|
@@ -288,72 +299,145 @@ export function installSentence(commands, to) {
|
|
|
288
299
|
? `First install dependencies: run ${listed(commands, 5)}.`
|
|
289
300
|
: `Dependencies are not installed. Run ${listed(commands, 5)} before the next scan.`;
|
|
290
301
|
}
|
|
302
|
+
/** A path as a reader types it, with the home directory as `~`. */
|
|
303
|
+
export function tilde(target, home = os.homedir()) {
|
|
304
|
+
if (target === home)
|
|
305
|
+
return "~";
|
|
306
|
+
return target.startsWith(`${home}${path.sep}`) ? `~${target.slice(home.length)}` : target;
|
|
307
|
+
}
|
|
291
308
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
309
|
+
* The files the write step creates, as one line of the list "Go ahead?" is
|
|
310
|
+
* asked about (carrick#1489, carrick#1512).
|
|
294
311
|
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
312
|
+
* Directories, because each holds several files and the line has to stay one
|
|
313
|
+
* sentence; every file outside the workspace by its own path, because those
|
|
314
|
+
* belong to somebody's editor. Claude Code's server list is written by its own
|
|
315
|
+
* command, so it is named rather than given a path. It says "add", because
|
|
316
|
+
* that is what happens: every writer here merges, and a line that listed
|
|
317
|
+
* folders read as though it replaced them. And it says where: a folder of
|
|
318
|
+
* repos gets the hooks and skills in the folder and again in each repo, with
|
|
319
|
+
* that repo's `.git/info/exclude` (carrick#1512, option A), so `folder` is the
|
|
320
|
+
* folder as the reader types it, and null for a run in one repo.
|
|
299
321
|
*/
|
|
300
|
-
export function
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
322
|
+
export function writesLine(options) {
|
|
323
|
+
const mcp = [
|
|
324
|
+
...(options.claudeCode ? ["Claude Code"] : []),
|
|
325
|
+
...options.editorFiles.map((file) => tilde(file, options.home)),
|
|
326
|
+
];
|
|
327
|
+
const where = options.folder === null ? "" : ` in ${options.folder} and in each repo`;
|
|
328
|
+
const parts = [
|
|
329
|
+
`Add Carrick's hooks and skills to .claude/, .agents/ and .codex/${where}`,
|
|
330
|
+
options.workspaceFile ? `create .carrick/ and ${WORKSPACE_FILE}` : "create .carrick/",
|
|
331
|
+
...(mcp.length > 0 ? [`add Carrick's MCP server to ${listed(mcp, 5)}`] : []),
|
|
332
|
+
];
|
|
333
|
+
return `${parts.slice(0, -1).join(", ")}, and ${parts.at(-1)}`;
|
|
334
|
+
}
|
|
335
|
+
/** The width a line of the "Next:" list wraps at, inside the gutter. */
|
|
336
|
+
export const NEXT_WIDTH = 68;
|
|
337
|
+
/**
|
|
338
|
+
* One line of the list, broken between words so that it stays inside the
|
|
339
|
+
* terminal's gutter: a line the terminal wraps for itself runs back to the
|
|
340
|
+
* first column, under the gutter, which is what the smoke run showed
|
|
341
|
+
* (carrick#1512).
|
|
342
|
+
*/
|
|
343
|
+
export function wrapped(text, width = NEXT_WIDTH) {
|
|
344
|
+
const lines = [];
|
|
345
|
+
let current = "";
|
|
346
|
+
for (const word of text.split(" ")) {
|
|
347
|
+
if (current !== "" && current.length + 1 + word.length > width) {
|
|
348
|
+
lines.push(current);
|
|
349
|
+
current = word;
|
|
305
350
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
};
|
|
309
|
-
for (const repo of input.selected) {
|
|
310
|
-
const key = header(repo);
|
|
311
|
-
groups.set(key, [...(groups.get(key) ?? []), repo]);
|
|
351
|
+
else
|
|
352
|
+
current = current === "" ? word : `${current} ${word}`;
|
|
312
353
|
}
|
|
313
|
-
|
|
354
|
+
if (current !== "")
|
|
355
|
+
lines.push(current);
|
|
356
|
+
return lines;
|
|
357
|
+
}
|
|
358
|
+
/** The list as it is printed: a heading, and each line indented and wrapped. */
|
|
359
|
+
export function nextBlock(lines) {
|
|
360
|
+
return ["Next:", ...lines.flatMap((line) => wrapped(line).map((part) => ` ${part}`))].join("\n");
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* The GitHub App line of that list: which repos still need the App.
|
|
364
|
+
*
|
|
365
|
+
* With a terminal, init opens the page itself once the reader says yes, so the
|
|
366
|
+
* line says that and no address. Without one it cannot, and the line carries
|
|
367
|
+
* the address instead: it is the only place a run with no terminal says it.
|
|
368
|
+
*/
|
|
369
|
+
export function connectItem(unconnected, total, interactive, url) {
|
|
370
|
+
const which = unconnected.length === total && total === 2
|
|
371
|
+
? "both repos"
|
|
372
|
+
: unconnected.length === total && total > 2
|
|
373
|
+
? `all ${total} repos`
|
|
374
|
+
: reposPhrase(unconnected);
|
|
375
|
+
return interactive
|
|
376
|
+
? `Open GitHub to install Carrick on ${which}`
|
|
377
|
+
: `Install the Carrick GitHub App on ${which}: ${url}`;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The moves out of other projects, one line per project they come from, in
|
|
381
|
+
* the spelling the move question uses too.
|
|
382
|
+
*/
|
|
383
|
+
export function moveLines(moving, project) {
|
|
384
|
+
return [...new Set(moving.map((entry) => entry.from))].map((from) => {
|
|
385
|
+
const repos = moving.filter((entry) => entry.from === from).map((entry) => entry.repo);
|
|
386
|
+
return `Move ${reposPhrase(repos)} from ${from} into ${project}`;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Everything the run will do, one line each, for the one question that covers
|
|
391
|
+
* it (carrick#1512).
|
|
392
|
+
*
|
|
393
|
+
* A project used to be created at its own question, before the reader had seen
|
|
394
|
+
* which repos went into it, and the GitHub App line was printed twice around
|
|
395
|
+
* the files question. Now the project, the App and the files are three lines
|
|
396
|
+
* under "Next:", and "Go ahead?" is asked once, about all of them. A move out
|
|
397
|
+
* of another project is a line here too, and still its own question after this
|
|
398
|
+
* one, because `--yes` never grants it (carrick#1338).
|
|
399
|
+
*/
|
|
400
|
+
export function nextLines(input) {
|
|
314
401
|
const lines = [];
|
|
315
|
-
|
|
316
|
-
lines.push(`
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const packages = found?.services.length ?? 0;
|
|
321
|
-
const from = repo.name === null ? null : (input.assigned.get(repo.name) ?? null);
|
|
322
|
-
const notes = [
|
|
323
|
-
repo.name ?? "no GitHub repo",
|
|
324
|
-
...(packages > 1 ? [`${packages} packages`] : []),
|
|
325
|
-
...(repo.name !== null && moving.has(repo.name.toLowerCase()) && from !== null
|
|
326
|
-
? [`moves from ${input.label(from)}`]
|
|
327
|
-
: []),
|
|
328
|
-
];
|
|
329
|
-
lines.push(` ${path.basename(repo.path).padEnd(width)} ${notes.join(" ")}`);
|
|
330
|
-
}
|
|
331
|
-
if (repos.length > 10)
|
|
332
|
-
lines.push(` and ${repos.length - 10} more`);
|
|
402
|
+
if (input.project !== null && input.create) {
|
|
403
|
+
lines.push(`Create project ${input.project} with ${reposPhrase(input.repos)}`);
|
|
404
|
+
}
|
|
405
|
+
else if (input.project !== null && input.joining.length > 0) {
|
|
406
|
+
lines.push(`Add ${reposPhrase(input.joining)} to project ${input.project}`);
|
|
333
407
|
}
|
|
408
|
+
if (input.project !== null) {
|
|
409
|
+
for (const line of moveLines(input.moving, input.project))
|
|
410
|
+
lines.push(line);
|
|
411
|
+
}
|
|
412
|
+
if (input.connect !== null)
|
|
413
|
+
lines.push(input.connect);
|
|
414
|
+
lines.push(input.writes);
|
|
334
415
|
return lines;
|
|
335
416
|
}
|
|
336
417
|
/**
|
|
337
|
-
* The
|
|
338
|
-
*
|
|
418
|
+
* The line a run inside one repo prints about the others beside it, where it
|
|
419
|
+
* does not ask about them: too many to list, or no terminal to ask in.
|
|
420
|
+
*/
|
|
421
|
+
export function thisRepoOnly(repo) {
|
|
422
|
+
return `Setting up ${repo} only. To set up several repos as one system, run carrick init in the folder that holds them.`;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Which of the repos beside this one belong to the same system, asked in the
|
|
426
|
+
* run itself (carrick#1512).
|
|
339
427
|
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* its
|
|
428
|
+
* The run used to name the folder above and tell the reader to start again
|
|
429
|
+
* there. This repo starts ticked and the others do not: a sibling folder is
|
|
430
|
+
* not a claim that the repos in it call each other. Each row names the GitHub
|
|
431
|
+
* repo its remote points at, which is what the reader recognises when two
|
|
432
|
+
* folders are called something else.
|
|
344
433
|
*/
|
|
345
|
-
export function
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
".codex/",
|
|
353
|
-
...(options.workspaceFile ? [WORKSPACE_FILE] : []),
|
|
354
|
-
...outside,
|
|
355
|
-
...(options.claudeCode ? ["Claude Code's MCP servers"] : []),
|
|
356
|
-
].join(", ")}`;
|
|
434
|
+
export async function chooseSiblings(current, siblings, directory, out) {
|
|
435
|
+
const rows = [current, ...siblings].map((repo) => ({
|
|
436
|
+
value: repo.path,
|
|
437
|
+
label: repo === current ? `${path.basename(repo.path)} (this repo)` : path.basename(repo.path),
|
|
438
|
+
hint: repo.name ?? "no GitHub remote",
|
|
439
|
+
}));
|
|
440
|
+
return out.choose(`${tilde(directory)} holds other repos. Which belong to the same system as ${path.basename(current.path)}?`, "a repo", rows, { initial: [current.path], required: true });
|
|
357
441
|
}
|
|
358
442
|
/**
|
|
359
443
|
* An end to the run with its own exit code, decided before anything is written.
|
|
@@ -556,11 +640,26 @@ export async function initWith(argv, out, interactive) {
|
|
|
556
640
|
process.stdout.write(`${parsed}\n`);
|
|
557
641
|
return parsed.startsWith("carrick init") ? 0 : 2;
|
|
558
642
|
}
|
|
559
|
-
|
|
643
|
+
// Where the run was started, and where it sets up: the folder above, once
|
|
644
|
+
// the reader has said a repo beside this one belongs with it (carrick#1512).
|
|
645
|
+
const startedIn = parsed.workspace;
|
|
646
|
+
let workspace = parsed.workspace;
|
|
560
647
|
if (!fs.existsSync(workspace)) {
|
|
561
648
|
process.stderr.write(`carrick init: ${workspace} is not a directory on this machine\n`);
|
|
562
649
|
return 1;
|
|
563
650
|
}
|
|
651
|
+
// A repo the folder above has already set up is set up from that folder
|
|
652
|
+
// again, rather than on its own beside it (carrick#1512).
|
|
653
|
+
const above = initFolderAbove(workspace);
|
|
654
|
+
if (above !== null) {
|
|
655
|
+
out.say(`${path.basename(realPath(workspace))} is set up with the repos in ${tilde(above)}, so this run sets up ${tilde(above)}.`);
|
|
656
|
+
workspace = above;
|
|
657
|
+
}
|
|
658
|
+
// How to run this again, naming the folder where the run set up a folder
|
|
659
|
+
// other than the one it started in.
|
|
660
|
+
const again = (flags = "") => realPath(workspace) === realPath(startedIn)
|
|
661
|
+
? `run carrick init${flags} again`
|
|
662
|
+
: `run carrick init${flags} in ${tilde(workspace)} again`;
|
|
564
663
|
// Everything this run would change is decided before any of it happens: the
|
|
565
664
|
// repos it covers, the project they belong in, and the moves that would
|
|
566
665
|
// take. `--project` used to move repos on the server as its first act and
|
|
@@ -609,7 +708,47 @@ export async function initWith(argv, out, interactive) {
|
|
|
609
708
|
// The derivation and the identities are local reads: they name the choice,
|
|
610
709
|
// and a run nobody has chosen in yet must not have signed anything in.
|
|
611
710
|
derived = deriveWorkspace(workspace);
|
|
612
|
-
|
|
711
|
+
let candidates = derived.plan.repos.map((repo) => repoIdentity(repo.path));
|
|
712
|
+
// The repos beside this one, asked about here rather than named with an
|
|
713
|
+
// instruction to start again in the folder above (carrick#1512). A yes to
|
|
714
|
+
// any of them carries on as if the run had started there: the same
|
|
715
|
+
// derivation, the same proposal, and the repos left unticked written to
|
|
716
|
+
// the same exclude list a folder run writes.
|
|
717
|
+
let ticked = null;
|
|
718
|
+
const parent = derived.plan.parent_proposal;
|
|
719
|
+
if (parent !== null && candidates.length === 1) {
|
|
720
|
+
const current = candidates[0];
|
|
721
|
+
const siblings = siblingRepos(derived.plan, excludedRepos(parent.directory));
|
|
722
|
+
const asking = siblings.length > 0 &&
|
|
723
|
+
siblings.length <= MAX_SIBLINGS &&
|
|
724
|
+
interactive &&
|
|
725
|
+
!parsed.assumeYes &&
|
|
726
|
+
parsed.repos.length === 0;
|
|
727
|
+
if (asking) {
|
|
728
|
+
const picked = new Set((await chooseSiblings(current, siblings.map((repo) => repoIdentity(repo)), parent.directory, out)).map(realPath));
|
|
729
|
+
if (picked.size > 1 || !picked.has(realPath(current.path))) {
|
|
730
|
+
workspace = parent.directory;
|
|
731
|
+
derived = deriveWorkspace(workspace);
|
|
732
|
+
candidates = derived.plan.repos.map((repo) => repoIdentity(repo.path));
|
|
733
|
+
const chosen = candidates.filter((repo) => picked.has(realPath(repo.path)));
|
|
734
|
+
// This repo first, where the reader kept it: it is the one they
|
|
735
|
+
// started in, and every sentence below names it first.
|
|
736
|
+
ticked = [
|
|
737
|
+
...chosen.filter((repo) => realPath(repo.path) === realPath(current.path)),
|
|
738
|
+
...chosen.filter((repo) => realPath(repo.path) !== realPath(current.path)),
|
|
739
|
+
];
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
else if (siblings.length > 0) {
|
|
743
|
+
out.say(thisRepoOnly(path.basename(current.path)));
|
|
744
|
+
}
|
|
745
|
+
else if (parent.repos.some((repo) => realPath(repo) === realPath(parent.directory))) {
|
|
746
|
+
// Not a folder of repos but a repository this directory sits inside,
|
|
747
|
+
// which is where a scan of it belongs: the one case where "run it in
|
|
748
|
+
// the folder above" is still the answer.
|
|
749
|
+
out.warn(`The parent folder ${parent.directory} is itself a repo. Run carrick init .. to initialise that workspace.`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
613
752
|
// What Carrick already knows about this folder, before the picker draws
|
|
614
753
|
// its defaults. A read, not a write: the credential has to exist already,
|
|
615
754
|
// so a machine nobody has signed in on still chooses first and signs in
|
|
@@ -623,10 +762,11 @@ export async function initWith(argv, out, interactive) {
|
|
|
623
762
|
const knownNames = [...new Set(candidates.map((repo) => repo.name).filter((name) => name !== null))];
|
|
624
763
|
// Only where a picker is actually going to draw. Every other path has its
|
|
625
764
|
// answer already — `--repo` names it, `--yes` takes the list as derived, a
|
|
626
|
-
// single repo is not a choice
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
|
|
765
|
+
// single repo is not a choice, the question about the repos beside this
|
|
766
|
+
// one was the choice — and asking about a repo whose name the reader never
|
|
767
|
+
// offered is a request that buys nothing (carrick#1338's test pins it: a
|
|
768
|
+
// repo left out is named in no request at all).
|
|
769
|
+
const picking = ticked === null && parsed.repos.length === 0 && !parsed.assumeYes && interactive && candidates.length > 1;
|
|
630
770
|
if (picking && saved !== null && knownNames.length > 0) {
|
|
631
771
|
try {
|
|
632
772
|
known = await resolveRepos(saved.token, knownNames);
|
|
@@ -637,13 +777,14 @@ export async function initWith(argv, out, interactive) {
|
|
|
637
777
|
projects = null;
|
|
638
778
|
}
|
|
639
779
|
}
|
|
640
|
-
const selected =
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
780
|
+
const selected = ticked ??
|
|
781
|
+
(await chooseRepos(derived.plan, candidates, {
|
|
782
|
+
repos: parsed.repos,
|
|
783
|
+
assumeYes: parsed.assumeYes,
|
|
784
|
+
interactive,
|
|
785
|
+
identity: known,
|
|
786
|
+
label: (slug) => projectLabel(slug, projects),
|
|
787
|
+
}, out));
|
|
647
788
|
if (selected.length === 0)
|
|
648
789
|
throw new Stop(0);
|
|
649
790
|
selectedRepos = selected;
|
|
@@ -653,7 +794,10 @@ export async function initWith(argv, out, interactive) {
|
|
|
653
794
|
.filter((repo) => !kept.has(repo.path))
|
|
654
795
|
.map((repo) => path.basename(repo.path));
|
|
655
796
|
const dropped = candidates.length - selected.length;
|
|
656
|
-
|
|
797
|
+
// Not after the question about the repos beside this one: its rows were
|
|
798
|
+
// the whole answer, and a folder repo that had no row there (one with no
|
|
799
|
+
// JavaScript or TypeScript in it) is not news.
|
|
800
|
+
if (dropped > 0 && ticked === null) {
|
|
657
801
|
out.done(`${selected.length} of ${candidates.length} repos covered; ${dropped} left out of the proposal, the project and the connection`);
|
|
658
802
|
}
|
|
659
803
|
// A machine that has never signed in signs in here rather than being told
|
|
@@ -719,7 +863,16 @@ export async function initWith(argv, out, interactive) {
|
|
|
719
863
|
// --project the step reads the assignment the repos already have and
|
|
720
864
|
// offers the list, rather than doing nothing at all (carrick#987).
|
|
721
865
|
const dashboard = workspaceUrls(initial.workspace.slug).projects;
|
|
866
|
+
// The repos by the folder names the reader has on disk, in the order the
|
|
867
|
+
// questions name them. Only those with a GitHub remote: a repo with none
|
|
868
|
+
// goes into no project, so no project line names it.
|
|
869
|
+
const named = selected.filter((repo) => repo.name !== null).map((repo) => path.basename(repo.path));
|
|
870
|
+
const shortName = (fullName) => {
|
|
871
|
+
const repo = selected.find((entry) => entry.name?.toLowerCase() === fullName.toLowerCase());
|
|
872
|
+
return repo === undefined ? fullName : path.basename(repo.path);
|
|
873
|
+
};
|
|
722
874
|
const prompts = {
|
|
875
|
+
repos: named,
|
|
723
876
|
say: out.say,
|
|
724
877
|
ask: out.ask,
|
|
725
878
|
confirm: out.confirm,
|
|
@@ -751,12 +904,6 @@ export async function initWith(argv, out, interactive) {
|
|
|
751
904
|
// scanner said about it. Everything above is a read; everything below the
|
|
752
905
|
// confirm is a write.
|
|
753
906
|
const plan = derived.plan;
|
|
754
|
-
if (plan.parent_proposal) {
|
|
755
|
-
const parent = plan.parent_proposal;
|
|
756
|
-
const folders = parent.repos.map((repo) => path.basename(repo));
|
|
757
|
-
const shown = folders.length > 3 ? `${folders.slice(0, 3).join(", ")} and ${folders.length - 3} more` : folders.join(", ");
|
|
758
|
-
out.warn(`The parent folder ${parent.directory} holds ${parent.repos.length} ${parent.repos.length === 1 ? "repo" : "repos"}: ${shown}. Run carrick init .. to initialise that workspace.`);
|
|
759
|
-
}
|
|
760
907
|
// The scanner's own warnings about what it proposed. Capped: the proposal
|
|
761
908
|
// file carries every one of them, and it is named on the line above.
|
|
762
909
|
const warnings = plan.repos.flatMap((repo) => repo.warnings);
|
|
@@ -773,33 +920,40 @@ export async function initWith(argv, out, interactive) {
|
|
|
773
920
|
// and one this machine has no command for is offered unticked.
|
|
774
921
|
const offered = offeredFileClients();
|
|
775
922
|
editors = await chooseEditors(offered, { editors: parsed.editors, assumeYes: parsed.assumeYes, interactive }, out);
|
|
776
|
-
//
|
|
777
|
-
// the
|
|
778
|
-
//
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
? `${decision.name} (${project})`
|
|
782
|
-
: project
|
|
783
|
-
: null;
|
|
784
|
-
for (const line of summaryBlocks({ plan, selected, project, created, label, assigned, moving }))
|
|
785
|
-
out.say(line);
|
|
923
|
+
// What the yes is given to: every thing this run will do, one line each —
|
|
924
|
+
// the project, the GitHub App, the files — and nothing created before it
|
|
925
|
+
// (carrick#1512). The files are named, editor files included, because a
|
|
926
|
+
// question that does not name a file is not consent to write it
|
|
927
|
+
// (carrick#1365, carrick#1489).
|
|
786
928
|
const unconnected = unconnectedRepos(initial, names);
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
929
|
+
// The project as the list and the move question both spell it: the name
|
|
930
|
+
// typed for one this run creates, the dashboard's label for one that is
|
|
931
|
+
// there.
|
|
932
|
+
const target = project === null ? null : decision.create ? (decision.name ?? project) : label(project);
|
|
933
|
+
const moves = moving.map((name) => ({ repo: shortName(name), from: label(assigned.get(name) ?? "") }));
|
|
934
|
+
const next = nextLines({
|
|
935
|
+
create: decision.create,
|
|
936
|
+
project: target,
|
|
937
|
+
repos: named,
|
|
938
|
+
joining: joining.map(shortName),
|
|
939
|
+
moving: moves,
|
|
940
|
+
connect: unconnected.length === 0
|
|
941
|
+
? null
|
|
942
|
+
: connectItem(unconnected.map(shortName), names.length, interactive, workspaceUrls(initial.workspace.slug).connect),
|
|
943
|
+
writes: writesLine({
|
|
944
|
+
workspaceFile: deselected.length > 0,
|
|
945
|
+
editorFiles: offered.filter((client) => editors.includes(client.name)).map((client) => client.file),
|
|
946
|
+
claudeCode: claudeCodeFound(),
|
|
947
|
+
// A folder of repos: the copy in each repo is written as well.
|
|
948
|
+
folder: selected.some((repo) => realPath(repo.path) !== realPath(workspace)) ? tilde(realPath(workspace)) : null,
|
|
949
|
+
}),
|
|
950
|
+
});
|
|
951
|
+
out.say(nextBlock(next));
|
|
795
952
|
if (!parsed.assumeYes) {
|
|
796
953
|
if (!interactive) {
|
|
797
954
|
throw new Stop(1, `use --yes to accept this proposal without a terminal${moving.length > 0 && !parsed.allowMove ? ", and --allow-move to accept the move above" : ""}.`);
|
|
798
955
|
}
|
|
799
|
-
|
|
800
|
-
// because a question that does not name a file is not consent to write
|
|
801
|
-
// it (carrick#1365, carrick#1489).
|
|
802
|
-
if (!(await out.confirm("Write these files?"))) {
|
|
956
|
+
if (!(await out.confirm("Go ahead?"))) {
|
|
803
957
|
out.refuse(NOTHING_WRITTEN);
|
|
804
958
|
throw new Stop(0);
|
|
805
959
|
}
|
|
@@ -808,12 +962,14 @@ export async function initWith(argv, out, interactive) {
|
|
|
808
962
|
// of local files; taking a repo out of a project changes what every agent
|
|
809
963
|
// querying that project can see, and that is not the same answer
|
|
810
964
|
// (carrick#1338).
|
|
811
|
-
if (moving.length > 0 &&
|
|
812
|
-
const from = [...new Set(
|
|
965
|
+
if (moving.length > 0 && target !== null && !parsed.allowMove) {
|
|
966
|
+
const from = [...new Set(moves.map((entry) => entry.from))].join(" and ");
|
|
967
|
+
const repos = reposPhrase(moves.map((entry) => entry.repo));
|
|
813
968
|
if (!interactive) {
|
|
814
|
-
throw new Stop(1, `${
|
|
969
|
+
throw new Stop(1, `${repos} ${moving.length === 1 ? "is" : "are"} in project ${from}. Moving ${moving.length === 1 ? "it" : "them"} into ${target} changes what every agent querying either project can see, so it needs --allow-move.`);
|
|
815
970
|
}
|
|
816
|
-
|
|
971
|
+
// The same repo and project spelling as the line in the list above it.
|
|
972
|
+
if (!(await out.confirm(`${moveLines(moves, target).join("; ")}?`))) {
|
|
817
973
|
out.refuse(NOTHING_WRITTEN);
|
|
818
974
|
throw new Stop(0);
|
|
819
975
|
}
|
|
@@ -846,18 +1002,16 @@ export async function initWith(argv, out, interactive) {
|
|
|
846
1002
|
// What the reader still has to do, said once at the end rather than between
|
|
847
1003
|
// the steps (carrick#1489).
|
|
848
1004
|
const todo = [];
|
|
849
|
-
let workspaceSlug = initial.workspace.slug;
|
|
850
1005
|
let projectExists = decision.exists;
|
|
851
1006
|
try {
|
|
852
1007
|
if (decision.create && project !== null) {
|
|
853
1008
|
const outcome = await createProject(credential.token, project, decision.name ?? project);
|
|
854
1009
|
if (outcome.kind === "created") {
|
|
855
|
-
// Known by its name from here on, the same as a listed one.
|
|
1010
|
+
// Known by its name from here on, the same as a listed one. Not said
|
|
1011
|
+
// on a line of its own: the list the reader said yes to named it,
|
|
1012
|
+
// and the line after the connection names it with its repos
|
|
1013
|
+
// (carrick#1512). A create the server refused is still said.
|
|
856
1014
|
projects = [...(projects ?? []), outcome.project];
|
|
857
|
-
// A workspace's first project takes the repos the GitHub App install
|
|
858
|
-
// staged (carrick-cloud#1359); any other starts empty.
|
|
859
|
-
const holds = outcome.project.repo_count;
|
|
860
|
-
out.done(`Created project ${label(project)}${holds > 0 ? ` with ${holds} repo${holds === 1 ? "" : "s"}` : ""}`);
|
|
861
1015
|
projectExists = true;
|
|
862
1016
|
}
|
|
863
1017
|
else if (outcome.kind === "refused") {
|
|
@@ -915,7 +1069,6 @@ export async function initWith(argv, out, interactive) {
|
|
|
915
1069
|
const command = hookCommand({ onPath: carrickOnPath });
|
|
916
1070
|
const settingsName = path.join(".claude", command.bare ? "settings.json" : "settings.local.json");
|
|
917
1071
|
const settingsFile = path.join(workspace, settingsName);
|
|
918
|
-
let hooksWritten = true;
|
|
919
1072
|
try {
|
|
920
1073
|
const settings = fs.existsSync(settingsFile) ? fs.readFileSync(settingsFile, "utf8") : null;
|
|
921
1074
|
const otherFile = path.join(workspace, ".claude", command.bare ? "settings.local.json" : "settings.json");
|
|
@@ -930,32 +1083,26 @@ export async function initWith(argv, out, interactive) {
|
|
|
930
1083
|
writeIfChanged(settingsFile, hooks.body);
|
|
931
1084
|
}
|
|
932
1085
|
catch (error) {
|
|
933
|
-
|
|
934
|
-
out.refuse(`Could not configure Carrick hooks: ${error.message}. Fix ${settingsName} and run carrick init again.`);
|
|
1086
|
+
out.refuse(`Could not configure Carrick hooks: ${error.message}. Fix ${settingsName} and ${again()}.`);
|
|
935
1087
|
}
|
|
936
1088
|
// The same two-part nudge for Codex, in the file Codex reads project hooks
|
|
937
1089
|
// from. Written for both hosts for the same reason the task skills are
|
|
938
1090
|
// (carrick#1335): which agent opens this workspace is not a thing init can
|
|
939
1091
|
// know, and a hook file for a host nobody runs costs nothing.
|
|
940
|
-
const hosts = hooksWritten ? ["Claude Code"] : [];
|
|
941
1092
|
try {
|
|
942
1093
|
if (writeCodexHooks(workspace, command.command) === "written") {
|
|
943
1094
|
todo.push("Codex asks you to trust the Carrick hooks the next time it starts; until you do, they do not run.");
|
|
944
1095
|
}
|
|
945
|
-
hosts.push("Codex");
|
|
946
1096
|
}
|
|
947
1097
|
catch (error) {
|
|
948
|
-
out.refuse(`Could not configure the Codex hooks: ${error.message}. Fix ${CODEX_HOOKS_FILE} and
|
|
1098
|
+
out.refuse(`Could not configure the Codex hooks: ${error.message}. Fix ${CODEX_HOOKS_FILE} and ${again()}.`);
|
|
949
1099
|
}
|
|
950
1100
|
// The task skills, beside the hooks and independent of them: a settings file
|
|
951
1101
|
// somebody hand-edited into invalid JSON is no reason to withhold the bodies
|
|
952
1102
|
// an agent reads. A file this package did not write, or one somebody has
|
|
953
1103
|
// since changed, is left alone and named.
|
|
954
|
-
let skills = false;
|
|
955
1104
|
try {
|
|
956
|
-
|
|
957
|
-
skills = done.length > 0;
|
|
958
|
-
todo.push(...warn);
|
|
1105
|
+
todo.push(...taskSkillWarnings(writeTaskSkills(workspace, { slug: projectSlug })));
|
|
959
1106
|
// Which build wrote them, so a hook running an older `carrick` than this
|
|
960
1107
|
// one says so instead of answering from files it does not match
|
|
961
1108
|
// (carrick#1372).
|
|
@@ -969,13 +1116,29 @@ export async function initWith(argv, out, interactive) {
|
|
|
969
1116
|
catch (error) {
|
|
970
1117
|
out.refuse(`Could not write the task skills: ${error.message}`);
|
|
971
1118
|
}
|
|
1119
|
+
// The same hooks and skills inside each repo of a folder, because an agent
|
|
1120
|
+
// started inside one of them reads neither host's files from the folder
|
|
1121
|
+
// above (carrick#1512, option A as ruled). Kept out of git through each
|
|
1122
|
+
// repo's own exclude file; a repo that is the workspace already has them.
|
|
1123
|
+
for (const repo of selectedRepos) {
|
|
1124
|
+
if (realPath(repo.path) === realPath(workspace))
|
|
1125
|
+
continue;
|
|
1126
|
+
try {
|
|
1127
|
+
// Null for a folder that is not a git repository of its own: its lines
|
|
1128
|
+
// would land in some other repository's exclude file.
|
|
1129
|
+
writeRepoCopy(repo.path, command.command, { slug: projectSlug });
|
|
1130
|
+
}
|
|
1131
|
+
catch (error) {
|
|
1132
|
+
out.refuse(`Could not add Carrick's hooks and skills to ${path.basename(repo.path)}: ${error.message}.`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
// Nothing here about restarting Claude Code: the instruction this run closes
|
|
1136
|
+
// on is pasted into a new agent session, which loads the MCP server it has
|
|
1137
|
+
// just been given (carrick#1512).
|
|
972
1138
|
const mcp = connectMcpClients(editors);
|
|
973
1139
|
// Nothing here about `carrick` not being on PATH: the offer above said it, in
|
|
974
1140
|
// front of the decision it changes, and with the command for this machine's
|
|
975
1141
|
// package manager rather than a guess at npm (carrick#1372).
|
|
976
|
-
if (mcp.some((outcome) => outcome.client === "Claude Code" && outcome.state === "written")) {
|
|
977
|
-
todo.push("Restart Claude Code to load the Carrick MCP server.");
|
|
978
|
-
}
|
|
979
1142
|
for (const outcome of mcp.filter((entry) => entry.state === "failed")) {
|
|
980
1143
|
todo.push(`MCP not configured for ${outcome.client}: ${outcome.detail}`);
|
|
981
1144
|
}
|
|
@@ -995,6 +1158,7 @@ export async function initWith(argv, out, interactive) {
|
|
|
995
1158
|
// grant page for repos that were already connected (carrick#1489).
|
|
996
1159
|
try {
|
|
997
1160
|
const current = await resolveRepos(credential.token, names);
|
|
1161
|
+
const role = current.workspace.role;
|
|
998
1162
|
const identity = await connectRepos(credential.token, names, current, {
|
|
999
1163
|
interactive,
|
|
1000
1164
|
project: project ?? undefined,
|
|
@@ -1002,16 +1166,18 @@ export async function initWith(argv, out, interactive) {
|
|
|
1002
1166
|
movable,
|
|
1003
1167
|
announce: announced,
|
|
1004
1168
|
label,
|
|
1169
|
+
// A role the read does not state is unknown, not a member's.
|
|
1170
|
+
member: role === "member",
|
|
1171
|
+
again: again(),
|
|
1005
1172
|
say: out.say,
|
|
1006
1173
|
track: (text, work, report) => out.step(text, work, report),
|
|
1007
1174
|
});
|
|
1008
|
-
workspaceSlug = identity.workspace.slug;
|
|
1009
1175
|
if (project !== null && !reposAreInProject(identity, names, project)) {
|
|
1010
1176
|
// An unverified project does not end the run, whether it was named on
|
|
1011
1177
|
// the command line or picked here. Stopping cost someone their hooks and
|
|
1012
1178
|
// their proposal for a browser step they could only take afterwards, and
|
|
1013
1179
|
// the documented command then needed two runs (carrick#993 row 8).
|
|
1014
|
-
todo.push(`Finish the browser steps above to put these repos in ${label(project)}, then
|
|
1180
|
+
todo.push(`Finish the browser steps above to put these repos in ${label(project)}, then ${again(` --project ${project}`)} to verify.`);
|
|
1015
1181
|
}
|
|
1016
1182
|
// After a wait that was stopped, the grant is still the thing to do, and
|
|
1017
1183
|
// the line that said so is above the wait.
|
|
@@ -1028,23 +1194,29 @@ export async function initWith(argv, out, interactive) {
|
|
|
1028
1194
|
// The rest of the project, which this machine does not hold. Carrick
|
|
1029
1195
|
// answers across every repo in a project, so a folder holding half of one
|
|
1030
1196
|
// is a partial index and nothing here would otherwise say so
|
|
1031
|
-
// (carrick#993 row 18).
|
|
1032
|
-
|
|
1197
|
+
// (carrick#993 row 18). "This machine" is every repo in the folder that
|
|
1198
|
+
// holds this run's repos, set up here or not: a repo in the folder beside
|
|
1199
|
+
// this one is not missing (carrick#1512). Only read where the run's own
|
|
1200
|
+
// repos leave something unaccounted for, because each is a git call.
|
|
1201
|
+
let absent = absentRepos(identity, names, project, label);
|
|
1202
|
+
if (absent.length > 0) {
|
|
1203
|
+
const folder = selectedRepos.some((repo) => realPath(repo.path) === realPath(workspace))
|
|
1204
|
+
? path.dirname(workspace)
|
|
1205
|
+
: workspace;
|
|
1206
|
+
const here = repoRoots(folder)
|
|
1207
|
+
.map((root) => repoIdentity(root).name)
|
|
1208
|
+
.filter((name) => name !== null);
|
|
1209
|
+
absent = absentRepos(identity, [...names, ...here], project, label);
|
|
1210
|
+
}
|
|
1211
|
+
todo.push(...absent);
|
|
1033
1212
|
}
|
|
1034
1213
|
catch (error) {
|
|
1035
1214
|
// The server's own sentence says what failed; this one says what is left.
|
|
1036
|
-
todo.push(`${error.message} The files here are written;
|
|
1215
|
+
todo.push(`${error.message} The files here are written; ${again()} to verify the connection and the project.`);
|
|
1037
1216
|
}
|
|
1038
1217
|
// What is set up, in one line; then the index; then what is left to do;
|
|
1039
1218
|
// then the instruction for the agent (carrick#1489).
|
|
1040
|
-
out.done(
|
|
1041
|
-
repos: selectedRepos.map((repo) => repo.name ?? path.basename(repo.path)),
|
|
1042
|
-
workspace: workspaceSlug,
|
|
1043
|
-
project: project === null ? null : label(project),
|
|
1044
|
-
hooks: hosts,
|
|
1045
|
-
skills,
|
|
1046
|
-
mcp: mcp.filter((outcome) => outcome.state !== "failed").map((outcome) => outcome.client),
|
|
1047
|
-
}));
|
|
1219
|
+
out.done(summaryLine(project === null ? null : label(project), selectedRepos.filter((repo) => repo.name !== null).map((repo) => path.basename(repo.path))));
|
|
1048
1220
|
// No paid scan ran here, and that is the point (carrick-cloud#799): the one
|
|
1049
1221
|
// scan runs against a config someone has read. Where CI has already built an
|
|
1050
1222
|
// index, this run reads it onto the machine instead — the hosted index is
|
|
@@ -1075,9 +1247,14 @@ export async function initWith(argv, out, interactive) {
|
|
|
1075
1247
|
const unscaffolded = reposToScaffold(selectedRepos);
|
|
1076
1248
|
if (unscaffolded.length > 0) {
|
|
1077
1249
|
const install = installSentence(installs, "agent");
|
|
1078
|
-
|
|
1250
|
+
// A new session, because that is the one that loads the MCP server this
|
|
1251
|
+
// run has just added (carrick#1512).
|
|
1252
|
+
out.note(NEXT_TITLE, [
|
|
1079
1253
|
...(install === null ? [] : [install]),
|
|
1080
1254
|
scaffoldSentence(unscaffolded),
|
|
1255
|
+
// The scaffold tool reads the proposal from the folder init ran in, and
|
|
1256
|
+
// a run that moved to the folder above did not run where it started.
|
|
1257
|
+
...(realPath(workspace) === realPath(startedIn) ? [] : [initFolderSentence(workspace)]),
|
|
1081
1258
|
]);
|
|
1082
1259
|
}
|
|
1083
1260
|
else {
|