@cspeach/cli 1.1.52 → 1.1.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/conversation-prompt.js +1 -0
- package/dist/agent/loop.js +150 -17
- package/dist/agent/maybe-build-project-context.js +11 -2
- package/dist/agent/repair-partial.js +9 -0
- package/dist/agent/tool-dispatch.js +17 -2
- package/dist/approvals/op-labels.js +6 -3
- package/dist/approvals/render.js +3 -2
- package/dist/auth/auth-file.js +3 -0
- package/dist/auth/key-resolver.js +8 -3
- package/dist/auth/me.js +6 -2
- package/dist/cli.js +22 -0
- package/dist/commands/login.js +22 -4
- package/dist/commands/plan-chain.js +2 -2
- package/dist/commands/plan-reground.js +5 -0
- package/dist/commands/plan-resume.js +2 -2
- package/dist/commands/plan-startup.js +3 -3
- package/dist/commands/sap-link.js +14 -1
- package/dist/commands/team-build-help.js +19 -0
- package/dist/commands/team-build.js +97 -0
- package/dist/commands/team-plan.js +2 -2
- package/dist/commands/team-spec.js +107 -0
- package/dist/commands/team.js +74 -4
- package/dist/docx/doc-meta.js +1 -0
- package/dist/docx/markdown-to-docx.js +2 -0
- package/dist/journey/chain.js +8 -0
- package/dist/journey/clean-core.js +2 -2
- package/dist/journey/rebuild.js +8 -0
- package/dist/journey/run.js +9 -0
- package/dist/lane/app-chain.js +13 -6
- package/dist/licence/paths.js +5 -0
- package/dist/licence/seat-kind.js +110 -0
- package/dist/licence/seat-state.js +4 -0
- package/dist/licence/terminal-seat.js +43 -0
- package/dist/licence/turn-gate.js +4 -0
- package/dist/one-shot.js +33 -0
- package/dist/project-context/conventions.js +8 -1
- package/dist/project-context/index-files.js +97 -34
- package/dist/project-context/index.js +1 -1
- package/dist/project-context/render.js +38 -4
- package/dist/projects/answer-blockers.js +133 -18
- package/dist/projects/blocker-answers-note.js +89 -0
- package/dist/projects/change-options.js +70 -0
- package/dist/projects/document-save.js +86 -0
- package/dist/projects/extract-design.js +4 -0
- package/dist/projects/extract-estimate.js +4 -0
- package/dist/projects/extract-spec-gap.js +8 -0
- package/dist/projects/index.js +1 -1
- package/dist/projects/plan-objects.js +24 -0
- package/dist/projects/plan-run.js +12 -2
- package/dist/projects/save-command.js +83 -20
- package/dist/projects/saved-lines.js +19 -17
- package/dist/projects/spec-share.js +239 -0
- package/dist/register/picture.js +2 -2
- package/dist/register/store.js +19 -0
- package/dist/renderer/change-card.js +14 -0
- package/dist/renderer/html-entities.js +25 -0
- package/dist/renderer/machine-blocks.js +54 -7
- package/dist/renderer/tool-labels.js +2 -0
- package/dist/renderer/tool-widget.js +20 -5
- package/dist/repl/builtin-commands.js +4 -3
- package/dist/repl/chat-lane.js +10 -1
- package/dist/repl/file-picker.js +52 -5
- package/dist/repl/mode-cycle.js +12 -0
- package/dist/repl/open-questions-gate.js +159 -0
- package/dist/repl/ready-phrase.js +108 -0
- package/dist/repl/startup-account.js +64 -1
- package/dist/repl/team-build-screen.js +116 -0
- package/dist/repl/team-build-start.js +178 -0
- package/dist/repl.js +216 -11
- package/dist/sap-link/policy.js +4 -0
- package/dist/sap-link/registry.js +9 -3
- package/dist/sap-link/server.js +4 -1
- package/dist/session/fresh-start.js +4 -0
- package/dist/session/user-prompt.js +2 -0
- package/dist/tools/_team-work-path.js +1 -1
- package/dist/tools/approval.js +12 -1
- package/dist/tools/cap/sap-link-tool.js +2 -0
- package/dist/tools/project/spec_answers_record.js +243 -0
- package/dist/tools/project/team_findings.js +2 -2
- package/dist/tools/seat-gate.js +41 -0
- package/dist/tools/tool-loading.js +2 -0
- package/dist/ui/approval-modal.js +2 -1
- package/dist/ui/widgets/ask-card.js +171 -29
- package/dist/ui/widgets/files-card.js +19 -1
- package/package.json +2 -2
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { formatFailure, formatInfo } from '../renderer/notice-shape.js';
|
|
2
|
+
import { resolveSeatKind, seatSignInMessage, isLegacySignIn } from '../licence/seat-kind.js';
|
|
2
3
|
export async function runStartupAccount(deps, io) {
|
|
3
4
|
let licenceCheckedAtMs;
|
|
5
|
+
let seat;
|
|
6
|
+
// The seat for this start; false = not confirmed (the caller exits 1).
|
|
7
|
+
const settleSeat = async (fresh, apiKey, authFile) => {
|
|
8
|
+
const r = resolveSeatKind({
|
|
9
|
+
fresh, stored: await deps.readSeatRecord(), apiKey, now: new Date((deps.now ?? Date.now)()), legacySignIn: isLegacySignIn(authFile),
|
|
10
|
+
});
|
|
11
|
+
if (!r.ok) {
|
|
12
|
+
io.closeBrand();
|
|
13
|
+
io.print(seatSignInMessage());
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (r.write)
|
|
17
|
+
await deps.writeSeatRecord(r.write).catch(() => { });
|
|
18
|
+
deps.setSeatKind(r.kind);
|
|
19
|
+
seat = r.kind;
|
|
20
|
+
return true;
|
|
21
|
+
};
|
|
4
22
|
try {
|
|
5
23
|
const cfgEarly = await deps.loadConfig();
|
|
6
24
|
if (cfgEarly.llm.mode === 'managed') {
|
|
@@ -14,9 +32,13 @@ export async function runStartupAccount(deps, io) {
|
|
|
14
32
|
io.print(formatFailure({ what: 'Your CSPeach sign-in is no longer valid', next: 'cspeach login' }));
|
|
15
33
|
return { exit: 1 };
|
|
16
34
|
}
|
|
35
|
+
if (!(await settleSeat(meResult, startupKey, authFile)))
|
|
36
|
+
return { exit: 1 };
|
|
17
37
|
io.notice(formatInfo(`Could not refresh your account (${meResult.error})`));
|
|
18
38
|
}
|
|
19
39
|
else {
|
|
40
|
+
if (!(await settleSeat(meResult, startupKey, authFile)))
|
|
41
|
+
return { exit: 1 };
|
|
20
42
|
io.showProfile(meResult.profile);
|
|
21
43
|
}
|
|
22
44
|
}
|
|
@@ -31,6 +53,11 @@ export async function runStartupAccount(deps, io) {
|
|
|
31
53
|
io.print(licence.message);
|
|
32
54
|
return { exit: 1 };
|
|
33
55
|
}
|
|
56
|
+
// The licence check IS the /v1/me call: an ok verdict carries the kind.
|
|
57
|
+
const fresh = licence.verdict === 'ok' && licence.profile
|
|
58
|
+
? { ok: true, profile: licence.profile, serverDate: null } : null;
|
|
59
|
+
if (!(await settleSeat(fresh, startupKey, await deps.readAuthFile())))
|
|
60
|
+
return { exit: 1 };
|
|
34
61
|
if (licence.verdict === 'unknown') {
|
|
35
62
|
io.notice(formatInfo(licence.line));
|
|
36
63
|
}
|
|
@@ -47,5 +74,41 @@ export async function runStartupAccount(deps, io) {
|
|
|
47
74
|
}
|
|
48
75
|
}
|
|
49
76
|
catch { /* banner is best-effort — never block REPL startup */ }
|
|
50
|
-
return
|
|
77
|
+
return {
|
|
78
|
+
exit: null,
|
|
79
|
+
...(licenceCheckedAtMs === undefined ? {} : { licenceCheckedAtMs }),
|
|
80
|
+
...(seat === undefined ? {} : { seat }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Team build Task 5, ruling R7a — start-up resolved no seat but a key exists
|
|
85
|
+
* (managed without auth.json, with a stored key or one pasted just now): one
|
|
86
|
+
* /v1/me for that key (fetchMe sends the CLI's user-agent itself), then the
|
|
87
|
+
* stored record, then the legacy rule (R7b). Null = not confirmed (exit 1).
|
|
88
|
+
* Runs only when start-up made no /v1/me call, so never a second one.
|
|
89
|
+
*/
|
|
90
|
+
export async function settleSeatAfterStartup(deps, a) {
|
|
91
|
+
let fresh = null;
|
|
92
|
+
try {
|
|
93
|
+
fresh = await deps.fetchMe({ proxyUrl: a.proxyUrl, apiKey: a.apiKey });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
fresh = null;
|
|
97
|
+
}
|
|
98
|
+
let authFile = null;
|
|
99
|
+
try {
|
|
100
|
+
authFile = await deps.readAuthFile();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
authFile = null;
|
|
104
|
+
}
|
|
105
|
+
const r = resolveSeatKind({
|
|
106
|
+
fresh, stored: await deps.readSeatRecord(), apiKey: a.apiKey, now: new Date((deps.now ?? Date.now)()), legacySignIn: isLegacySignIn(authFile),
|
|
107
|
+
});
|
|
108
|
+
if (!r.ok)
|
|
109
|
+
return null;
|
|
110
|
+
if (r.write)
|
|
111
|
+
await deps.writeSeatRecord(r.write).catch(() => { });
|
|
112
|
+
deps.setSeatKind(r.kind);
|
|
113
|
+
return r.kind;
|
|
51
114
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team build (spec 2026-10-25) — the developer start screen.
|
|
3
|
+
*
|
|
4
|
+
* When CSPeach starts in a team build folder, it says what sits in the shared
|
|
5
|
+
* `specs/` folder: each spec ready for build with its summary and open
|
|
6
|
+
* questions, and one line for each draft or spec being built. A developer is
|
|
7
|
+
* then offered each ready spec: build it, ask about it first, or later. A
|
|
8
|
+
* functional seat sees the same lines and is never offered a build.
|
|
9
|
+
*
|
|
10
|
+
* Ruling R10: the build folder is the folder CSPeach runs in, never
|
|
11
|
+
* `activeProject()`. Ruling R12: the status record goes to the spec folder.
|
|
12
|
+
* Never blocks start-up: a problem is one line.
|
|
13
|
+
*/
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
const day = (iso) => String(iso ?? '').slice(0, 10);
|
|
16
|
+
export function startScreenLines(specs, _seat) {
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const s of specs) {
|
|
19
|
+
const who = `"${s.title}" — ${s.author}, ${day(s.createdAt)}`;
|
|
20
|
+
if (s.status === 'ready for build') {
|
|
21
|
+
out.push(`Ready to build: ${who}`, ` ${s.summary}`);
|
|
22
|
+
if (s.openQuestions.length) {
|
|
23
|
+
out.push(' Open questions:');
|
|
24
|
+
for (const q of s.openQuestions)
|
|
25
|
+
out.push(` - [${q.severity}] ${q.text}`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
out.push(' No open questions.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
out.push(`${s.status === 'draft' ? 'Draft' : 'Being built'}: ${who}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
const BUILD = 'Build this spec';
|
|
38
|
+
const ASK = 'Ask about it first';
|
|
39
|
+
const LATER = 'Later';
|
|
40
|
+
export const askSentence = (s) => `Tell me about the spec "${s.title}" in specs/${s.slug}/ before we build it.`;
|
|
41
|
+
async function chooseFor(choose, s) {
|
|
42
|
+
const n = s.openQuestions.length;
|
|
43
|
+
const v = await choose({
|
|
44
|
+
title: `Ready to build: "${s.title}"`,
|
|
45
|
+
facts: [`${s.author}, ${day(s.createdAt)}`, n === 0 ? 'No open questions.' : n === 1 ? '1 open question.' : `${n} open questions.`],
|
|
46
|
+
choices: [{ value: 'build', label: BUILD }, { value: 'ask', label: ASK }, { value: 'later', label: LATER }],
|
|
47
|
+
});
|
|
48
|
+
if (v === 'build')
|
|
49
|
+
return { kind: 'build', spec: s };
|
|
50
|
+
if (v === 'ask')
|
|
51
|
+
return { kind: 'ask', spec: s };
|
|
52
|
+
return { kind: 'later' };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One card per ready spec, developers only. Build or Ask submits one command
|
|
56
|
+
* and ends the offer (one turn at a time); Later moves to the next spec.
|
|
57
|
+
* Returns true when a command was submitted.
|
|
58
|
+
*/
|
|
59
|
+
export async function offerBuild(a) {
|
|
60
|
+
if (a.seat !== 'developer')
|
|
61
|
+
return false;
|
|
62
|
+
for (const s of a.specs) {
|
|
63
|
+
if (s.status !== 'ready for build')
|
|
64
|
+
continue;
|
|
65
|
+
const c = await chooseFor(a.choose, s);
|
|
66
|
+
if (c.kind === 'later')
|
|
67
|
+
continue;
|
|
68
|
+
if (c.kind === 'ask') {
|
|
69
|
+
a.submit(askSentence(s));
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
const { chainFromToken } = await import('../lane/chain-token.js');
|
|
73
|
+
const t = await chainFromToken(a.teamDir, s.envelopePath);
|
|
74
|
+
if (!t) {
|
|
75
|
+
a.emit(`We could not start the build from specs/${s.slug}/. Run /abap-app --from @specs/${s.slug}/<file> yourself.`);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const { writeStatusRecord } = await import('../projects/spec-share.js');
|
|
79
|
+
writeStatusRecord(s.specDir, {
|
|
80
|
+
projectId: a.projectId, slug: s.slug, file: path.basename(s.envelopePath),
|
|
81
|
+
status: 'being built', by: a.by, at: (a.now ?? (() => new Date()))().toISOString(),
|
|
82
|
+
});
|
|
83
|
+
// R14: a Stop at the open-questions gate puts it back to ready for build.
|
|
84
|
+
const { noteBeingBuiltThisRun } = await import('./open-questions-gate.js');
|
|
85
|
+
noteBeingBuiltThisRun(s.specDir);
|
|
86
|
+
a.submit(`/abap-app --from ${t.token}`);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The start-up step: the build folder found by cwd (R10), its specs listed,
|
|
93
|
+
* the offer made. Never throws; at most one line on a problem.
|
|
94
|
+
*/
|
|
95
|
+
export async function runStartScreen(a) {
|
|
96
|
+
try {
|
|
97
|
+
const { buildFolderAt } = await import('../commands/team-spec.js');
|
|
98
|
+
const here = buildFolderAt(a.cwd);
|
|
99
|
+
if (!here)
|
|
100
|
+
return { submitted: false };
|
|
101
|
+
const { listSharedSpecs, specAuthorFor } = await import('../projects/spec-share.js');
|
|
102
|
+
const specs = listSharedSpecs(here.dir);
|
|
103
|
+
for (const l of startScreenLines(specs, a.seat))
|
|
104
|
+
a.emit(l);
|
|
105
|
+
const submitted = await offerBuild({
|
|
106
|
+
specs, seat: a.seat, choose: a.choose, teamDir: here.dir, projectId: here.project.projectId,
|
|
107
|
+
by: specAuthorFor(here.project.projectId, a.identityName),
|
|
108
|
+
submit: a.submit, emit: a.emit, ...(a.now ? { now: a.now } : {}),
|
|
109
|
+
});
|
|
110
|
+
return { submitted };
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
a.emit(`Could not show the team build specs: ${(e instanceof Error ? e.message : String(e)).split('\n')[0]}`);
|
|
114
|
+
return { submitted: false };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team build (spec 2026-10-25 §7.2) — the folder sets itself up when someone
|
|
3
|
+
* types `cspeach`. Nobody types a team command:
|
|
4
|
+
*
|
|
5
|
+
* - a functional seat in a folder that is not a team project gets a team
|
|
6
|
+
* build, with one line. A folder that already holds files asks first
|
|
7
|
+
* (default No), so opening CSPeach in Documents by mistake changes nothing.
|
|
8
|
+
* - a developer is offered a team build once per folder (default No), never
|
|
9
|
+
* in the home folder. The answer is remembered, so a scratch folder sees
|
|
10
|
+
* the question once and nothing changes.
|
|
11
|
+
* - anyone in a team build folder they have not joined is joined, one line.
|
|
12
|
+
* - an old build folder gets the one "build type changed" line.
|
|
13
|
+
* - clean-core and upgrade folders: nothing at all, as today.
|
|
14
|
+
*
|
|
15
|
+
* Runs at start-up only in an interactive run, after the seat is settled.
|
|
16
|
+
* Never throws: a problem is one line, and CSPeach starts as usual.
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { TEAM_FILE, activeProject, readProjectFile, } from '../register/store.js';
|
|
21
|
+
import { LEGACY_BUILD_LINE, createBuildProject, isBuildProject, isLegacyBuildFolder } from '../commands/team-build.js';
|
|
22
|
+
import { runTeamCommand } from '../commands/team.js';
|
|
23
|
+
import { resolveProjectFolderSync } from '../projects/project-folder.js';
|
|
24
|
+
import { normDir } from '../session/project-match.js';
|
|
25
|
+
import { cspeachRoot } from '../config/paths.js';
|
|
26
|
+
import { writeWhole } from '../register/write-whole.js';
|
|
27
|
+
export const SETUP_LINE = 'Set up as a team build folder. Your specs save here for the developer.';
|
|
28
|
+
export const FILES_QUESTION = 'This folder already has files in it. Set it up as a team build folder?';
|
|
29
|
+
export const NOT_SET_UP_LINE = 'Not set up. Start CSPeach in an empty folder for the team build.';
|
|
30
|
+
export const DEVELOPER_OFFER = 'Set this folder up as a team build (one shared folder for the spec and the build)?';
|
|
31
|
+
export const joinedLine = (name, lead) => `Joined the team build "${name}" (lead ${lead}).`;
|
|
32
|
+
/** What CSPeach itself leaves in a folder: none of it means "this folder holds files". */
|
|
33
|
+
const OURS = new Set(['.cspeach', 'cspeach-out', '.git', '.gitignore']);
|
|
34
|
+
export function folderState(cwd) {
|
|
35
|
+
const dir = path.resolve(cwd);
|
|
36
|
+
if (!fs.existsSync(path.join(dir, TEAM_FILE))) {
|
|
37
|
+
let entries = [];
|
|
38
|
+
try {
|
|
39
|
+
entries = fs.readdirSync(dir);
|
|
40
|
+
}
|
|
41
|
+
catch { /* unreadable folder: treat as empty of files */ }
|
|
42
|
+
return { kind: 'not-team', hasFiles: entries.some((e) => !OURS.has(e)), isHome: resolveProjectFolderSync(dir).kind === 'home' };
|
|
43
|
+
}
|
|
44
|
+
let project;
|
|
45
|
+
try {
|
|
46
|
+
project = readProjectFile(dir);
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
return { kind: 'unreadable', line: e instanceof Error ? e.message : String(e) };
|
|
50
|
+
}
|
|
51
|
+
if (isBuildProject(project)) {
|
|
52
|
+
const joined = activeProject();
|
|
53
|
+
return { kind: 'build', project, joined: !!joined && joined.projectId === project.projectId && normDir(joined.projectDir) === normDir(dir) };
|
|
54
|
+
}
|
|
55
|
+
if (isLegacyBuildFolder(project))
|
|
56
|
+
return { kind: 'legacy-build' };
|
|
57
|
+
return { kind: 'other-team' };
|
|
58
|
+
}
|
|
59
|
+
const offersFile = () => path.join(cspeachRoot(), 'team-build-offers.json');
|
|
60
|
+
function readOffered() {
|
|
61
|
+
try {
|
|
62
|
+
const raw = fs.readFileSync(offersFile(), 'utf8');
|
|
63
|
+
const j = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
|
|
64
|
+
return Array.isArray(j?.offered) ? j.offered.filter((x) => typeof x === 'string') : [];
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** `<CSPEACH_HOME>/team-build-offers.json`: `{ offered: [normDir, …] }`. */
|
|
71
|
+
export function fileOfferMemory() {
|
|
72
|
+
return {
|
|
73
|
+
wasOffered: async (dir) => readOffered().includes(normDir(dir)),
|
|
74
|
+
markOffered: async (dir) => {
|
|
75
|
+
const offered = readOffered();
|
|
76
|
+
const k = normDir(dir);
|
|
77
|
+
if (offered.includes(k))
|
|
78
|
+
return;
|
|
79
|
+
writeWhole(offersFile(), `${JSON.stringify({ offered: [...offered, k] }, null, 2)}\n`);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Ruling R10: CSPeach joins a team build by itself only when no project is
|
|
85
|
+
* active, or the active one is a team build too. A clean-core or upgrade
|
|
86
|
+
* project this laptop works on stays the active one: the journey and the team
|
|
87
|
+
* tools read `activeProject()`. An active project we cannot read is left
|
|
88
|
+
* alone as well.
|
|
89
|
+
*/
|
|
90
|
+
export function mayAutoJoin() {
|
|
91
|
+
try {
|
|
92
|
+
const active = activeProject();
|
|
93
|
+
if (!active)
|
|
94
|
+
return true;
|
|
95
|
+
return isBuildProject(readProjectFile(active.projectDir));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Join through the same route as `cspeach team join`, quietly. Failure: its own line(s). */
|
|
102
|
+
async function joinHere(dir, a) {
|
|
103
|
+
const said = [];
|
|
104
|
+
const deps = {
|
|
105
|
+
print: (l) => said.push(l),
|
|
106
|
+
identity: () => ({ name: a.identity.name }),
|
|
107
|
+
...(a.now ? { now: a.now } : {}),
|
|
108
|
+
};
|
|
109
|
+
const code = await runTeamCommand(['join', dir, '--outbox', dir], deps);
|
|
110
|
+
if (code !== 0)
|
|
111
|
+
for (const l of said)
|
|
112
|
+
a.emit(l);
|
|
113
|
+
return code === 0;
|
|
114
|
+
}
|
|
115
|
+
async function setUp(dir, a) {
|
|
116
|
+
const lead = a.identity.account ? { name: a.identity.name, account: a.identity.account } : { name: a.identity.name };
|
|
117
|
+
const project = createBuildProject(dir, {
|
|
118
|
+
name: path.basename(dir) || 'Team build',
|
|
119
|
+
lead,
|
|
120
|
+
now: (a.now ?? (() => new Date()))(),
|
|
121
|
+
...(a.rand ? { rand: a.rand } : {}),
|
|
122
|
+
});
|
|
123
|
+
if (mayAutoJoin() && !(await joinHere(dir, a)))
|
|
124
|
+
return null;
|
|
125
|
+
a.emit(SETUP_LINE);
|
|
126
|
+
return project;
|
|
127
|
+
}
|
|
128
|
+
export async function startTeamBuildFolder(a) {
|
|
129
|
+
const dir = path.resolve(a.cwd);
|
|
130
|
+
try {
|
|
131
|
+
const state = folderState(dir);
|
|
132
|
+
switch (state.kind) {
|
|
133
|
+
case 'other-team': return { project: null };
|
|
134
|
+
case 'unreadable':
|
|
135
|
+
a.emit(state.line);
|
|
136
|
+
return { project: null };
|
|
137
|
+
case 'legacy-build':
|
|
138
|
+
a.emit(LEGACY_BUILD_LINE);
|
|
139
|
+
return { project: null };
|
|
140
|
+
case 'build': {
|
|
141
|
+
if (state.joined || !mayAutoJoin())
|
|
142
|
+
return { project: state.project };
|
|
143
|
+
if (!(await joinHere(dir, a)))
|
|
144
|
+
return { project: null };
|
|
145
|
+
a.emit(joinedLine(state.project.name, state.project.lead.name));
|
|
146
|
+
return { project: state.project };
|
|
147
|
+
}
|
|
148
|
+
case 'not-team': {
|
|
149
|
+
if (a.seat === 'functional') {
|
|
150
|
+
// The home folder is never a team build: it is where CSPeach keeps its own files.
|
|
151
|
+
if (state.isHome) {
|
|
152
|
+
a.emit(NOT_SET_UP_LINE);
|
|
153
|
+
return { project: null };
|
|
154
|
+
}
|
|
155
|
+
if (state.hasFiles && (await a.confirm(FILES_QUESTION, { default: 'n' })) !== 'y') {
|
|
156
|
+
a.emit(NOT_SET_UP_LINE);
|
|
157
|
+
return { project: null };
|
|
158
|
+
}
|
|
159
|
+
return { project: await setUp(dir, a) };
|
|
160
|
+
}
|
|
161
|
+
if (state.isHome || (await a.memory.wasOffered(dir)))
|
|
162
|
+
return { project: null };
|
|
163
|
+
const answer = await a.confirm(DEVELOPER_OFFER, { default: 'n' });
|
|
164
|
+
try {
|
|
165
|
+
await a.memory.markOffered(dir);
|
|
166
|
+
}
|
|
167
|
+
catch { /* a failed offer write says nothing */ }
|
|
168
|
+
if (answer !== 'y')
|
|
169
|
+
return { project: null };
|
|
170
|
+
return { project: await setUp(dir, a) };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
a.emit(e instanceof Error ? e.message.split('\n')[0] : String(e));
|
|
176
|
+
return { project: null };
|
|
177
|
+
}
|
|
178
|
+
}
|