@brainervirus/workit-cli 0.5.1 → 0.5.3
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/index.js +1303 -0
- package/package.json +8 -6
- package/src/index.tsx +0 -35
- package/src/logic.ts +0 -263
- package/src/steps.tsx +0 -605
package/src/steps.tsx
DELETED
|
@@ -1,605 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useInput } from "ink";
|
|
2
|
-
import { ConfirmInput, MultiSelect, Select, TextInput } from "@inkjs/ui";
|
|
3
|
-
import { useState, type Dispatch, type JSX, type SetStateAction } from "react";
|
|
4
|
-
import { configDir, readConfig, writeConfig, type BranchPreset, type ToolkitConfig } from "@brainervirus/workit-core/src/core/config.ts";
|
|
5
|
-
import type { WorkspaceConfig } from "@brainervirus/workit-core/src/core/workspaces.ts";
|
|
6
|
-
import {
|
|
7
|
-
collectConfigValues,
|
|
8
|
-
DEFAULT_BASE_URL,
|
|
9
|
-
loadWorkspaces,
|
|
10
|
-
parseList,
|
|
11
|
-
runProjectSetup,
|
|
12
|
-
scaffoldVcs,
|
|
13
|
-
scaffoldYouTrack,
|
|
14
|
-
shouldWriteWorkspaces,
|
|
15
|
-
validateBaseUrl,
|
|
16
|
-
validateLocale,
|
|
17
|
-
validateTimezone,
|
|
18
|
-
writeWorkspaces,
|
|
19
|
-
type ProjectSetupResult,
|
|
20
|
-
type VcsProvider,
|
|
21
|
-
type VcsScaffold,
|
|
22
|
-
type YouTrackScaffold,
|
|
23
|
-
} from "./logic";
|
|
24
|
-
|
|
25
|
-
export type WizardResults = {
|
|
26
|
-
platforms: string[];
|
|
27
|
-
config: ToolkitConfig;
|
|
28
|
-
workspaces: WorkspaceConfig[];
|
|
29
|
-
youtrack: YouTrackScaffold | null;
|
|
30
|
-
vcs: VcsScaffold | null;
|
|
31
|
-
project: ProjectSetupResult | null;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type StepProps = {
|
|
35
|
-
results: WizardResults;
|
|
36
|
-
setResults: Dispatch<SetStateAction<WizardResults>>;
|
|
37
|
-
onDone: () => void;
|
|
38
|
-
onExit: () => void;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
type StepComponent = (props: StepProps) => JSX.Element;
|
|
42
|
-
|
|
43
|
-
const PLATFORMS = [
|
|
44
|
-
{ label: "OpenCode", value: "opencode" },
|
|
45
|
-
{ label: "Cursor", value: "cursor" },
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
const BRANCH_PRESETS: { label: string; value: BranchPreset }[] = [
|
|
49
|
-
{ label: "GitFlow", value: "gitflow" },
|
|
50
|
-
{ label: "GitHub Flow", value: "github-flow" },
|
|
51
|
-
{ label: "Trunk-based", value: "trunk-based" },
|
|
52
|
-
{ label: "Custom", value: "custom" },
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
const VCS_PROVIDERS = [
|
|
56
|
-
{ label: "GitLab", value: "gitlab" },
|
|
57
|
-
{ label: "GitHub", value: "github" },
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
function continueLabel(): string {
|
|
61
|
-
return " y to continue · n to stay · Esc to exit";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function Wizard({ onExit }: { onExit: () => void }): JSX.Element {
|
|
65
|
-
const [step, setStep] = useState(0);
|
|
66
|
-
const [results, setResults] = useState<WizardResults>(() => ({
|
|
67
|
-
platforms: [],
|
|
68
|
-
config: readConfig(),
|
|
69
|
-
workspaces: [],
|
|
70
|
-
youtrack: null,
|
|
71
|
-
vcs: null,
|
|
72
|
-
project: null,
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
useInput((input, key) => {
|
|
76
|
-
if (key.escape || (key.ctrl && input.toLowerCase() === "c")) onExit();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const advance = () => setStep((s) => Math.min(s + 1, 6));
|
|
80
|
-
const props: StepProps = { results, setResults, onDone: advance, onExit };
|
|
81
|
-
const Step = (step === 6 ? SummaryStep : [PlatformStep, ConfigStep, YouTrackStep, VcsStep, WorkspacesStep, ProjectStep, SummaryStep][step]) as StepComponent;
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<Box flexDirection="column" gap={1}>
|
|
85
|
-
<Text bold color="cyan">
|
|
86
|
-
flowkit — workflow rails for agentic coding
|
|
87
|
-
</Text>
|
|
88
|
-
<Step {...props} />
|
|
89
|
-
</Box>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function PlatformStep({ results, setResults, onDone }: StepProps): JSX.Element {
|
|
94
|
-
const [error, setError] = useState(false);
|
|
95
|
-
return (
|
|
96
|
-
<Box flexDirection="column" gap={1}>
|
|
97
|
-
<Text bold>Step 1 — Platforms</Text>
|
|
98
|
-
<Text dimColor>Select the tools to configure (space to toggle):</Text>
|
|
99
|
-
<MultiSelect
|
|
100
|
-
options={PLATFORMS}
|
|
101
|
-
defaultValue={results.platforms}
|
|
102
|
-
onSubmit={(values) => {
|
|
103
|
-
if (values.length === 0) {
|
|
104
|
-
setError(true);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
setResults((r) => ({ ...r, platforms: values }));
|
|
108
|
-
onDone();
|
|
109
|
-
}}
|
|
110
|
-
/>
|
|
111
|
-
{error && <Text color="red">Select at least one platform to continue.</Text>}
|
|
112
|
-
<Text dimColor>Enter to continue · Esc to exit</Text>
|
|
113
|
-
</Box>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function ConfigStep({ results, setResults, onDone }: StepProps): JSX.Element {
|
|
118
|
-
const current = results.config;
|
|
119
|
-
const [locale, setLocale] = useState(current.locale);
|
|
120
|
-
const [localeOk, setLocaleOk] = useState(validateLocale(current.locale) === null);
|
|
121
|
-
const [localeError, setLocaleError] = useState<string | null>(null);
|
|
122
|
-
const [timezone, setTimezone] = useState(current.timezone);
|
|
123
|
-
const [tzOk, setTzOk] = useState(validateTimezone(current.timezone) === null);
|
|
124
|
-
const [tzError, setTzError] = useState<string | null>(null);
|
|
125
|
-
const [preset, setPreset] = useState<BranchPreset>(current.branchPolicy.preset);
|
|
126
|
-
const [allowed, setAllowed] = useState(current.branchPolicy.allowed.join(", "));
|
|
127
|
-
const [protectedNames, setProtectedNames] = useState(current.branchPolicy.protected.join(", "));
|
|
128
|
-
|
|
129
|
-
const save = () => {
|
|
130
|
-
const next = collectConfigValues(
|
|
131
|
-
{
|
|
132
|
-
locale: localeOk ? locale : undefined,
|
|
133
|
-
timezone: tzOk ? timezone : undefined,
|
|
134
|
-
preset,
|
|
135
|
-
allowed: preset === "custom" ? parseList(allowed) : undefined,
|
|
136
|
-
protectedNames: preset === "custom" ? parseList(protectedNames) : undefined,
|
|
137
|
-
},
|
|
138
|
-
current,
|
|
139
|
-
);
|
|
140
|
-
writeConfig(next);
|
|
141
|
-
setResults((r) => ({ ...r, config: next }));
|
|
142
|
-
onDone();
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
return (
|
|
146
|
-
<Box flexDirection="column" gap={1}>
|
|
147
|
-
<Text bold>Step 2 — Global config</Text>
|
|
148
|
-
<Text dimColor>Locale (BCP-47, e.g. en or es-CL):</Text>
|
|
149
|
-
<TextInput
|
|
150
|
-
defaultValue={locale}
|
|
151
|
-
onSubmit={(v) => {
|
|
152
|
-
const err = validateLocale(v);
|
|
153
|
-
if (err) {
|
|
154
|
-
setLocaleError(err);
|
|
155
|
-
setLocaleOk(false);
|
|
156
|
-
} else {
|
|
157
|
-
setLocaleError(null);
|
|
158
|
-
setLocale(v);
|
|
159
|
-
setLocaleOk(true);
|
|
160
|
-
}
|
|
161
|
-
}}
|
|
162
|
-
/>
|
|
163
|
-
{localeError && <Text color="red">{localeError}</Text>}
|
|
164
|
-
<Text dimColor>Timezone (IANA name, e.g. America/Santiago):</Text>
|
|
165
|
-
<TextInput
|
|
166
|
-
defaultValue={timezone}
|
|
167
|
-
onSubmit={(v) => {
|
|
168
|
-
const err = validateTimezone(v);
|
|
169
|
-
if (err) {
|
|
170
|
-
setTzError(err);
|
|
171
|
-
setTzOk(false);
|
|
172
|
-
} else {
|
|
173
|
-
setTzError(null);
|
|
174
|
-
setTimezone(v);
|
|
175
|
-
setTzOk(true);
|
|
176
|
-
}
|
|
177
|
-
}}
|
|
178
|
-
/>
|
|
179
|
-
{tzError && <Text color="red">{tzError}</Text>}
|
|
180
|
-
<Text dimColor>Branch policy preset:</Text>
|
|
181
|
-
<Select options={BRANCH_PRESETS} defaultValue={preset} onChange={(v) => setPreset(v as BranchPreset)} />
|
|
182
|
-
{preset === "custom" && (
|
|
183
|
-
<>
|
|
184
|
-
<Text dimColor>Allowed branch patterns (comma-separated):</Text>
|
|
185
|
-
<TextInput defaultValue={allowed} onChange={setAllowed} />
|
|
186
|
-
<Text dimColor>Protected branch names (comma-separated):</Text>
|
|
187
|
-
<TextInput defaultValue={protectedNames} onChange={setProtectedNames} />
|
|
188
|
-
</>
|
|
189
|
-
)}
|
|
190
|
-
<ConfirmInput
|
|
191
|
-
isDisabled={!localeOk || !tzOk}
|
|
192
|
-
defaultChoice="confirm"
|
|
193
|
-
submitOnEnter={false}
|
|
194
|
-
onConfirm={save}
|
|
195
|
-
onCancel={() => {}}
|
|
196
|
-
/>
|
|
197
|
-
<Text dimColor>{continueLabel()}</Text>
|
|
198
|
-
</Box>
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function YouTrackStep({ results, setResults, onDone }: StepProps): JSX.Element {
|
|
203
|
-
const [baseUrl, setBaseUrl] = useState(DEFAULT_BASE_URL);
|
|
204
|
-
const [urlError, setUrlError] = useState<string | null>(null);
|
|
205
|
-
const [scaffold, setScaffold] = useState<YouTrackScaffold | null>(results.youtrack);
|
|
206
|
-
|
|
207
|
-
return (
|
|
208
|
-
<Box flexDirection="column" gap={1}>
|
|
209
|
-
<Text bold>Step 3 — YouTrack</Text>
|
|
210
|
-
<Text dimColor>Base URL (https):</Text>
|
|
211
|
-
<TextInput
|
|
212
|
-
defaultValue={baseUrl}
|
|
213
|
-
isDisabled={scaffold !== null}
|
|
214
|
-
onSubmit={(v) => {
|
|
215
|
-
const err = validateBaseUrl(v);
|
|
216
|
-
if (err) {
|
|
217
|
-
setUrlError(err);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
setUrlError(null);
|
|
221
|
-
setBaseUrl(v);
|
|
222
|
-
const s = scaffoldYouTrack(configDir(), v, {
|
|
223
|
-
locale: results.config.locale,
|
|
224
|
-
timezone: results.config.timezone,
|
|
225
|
-
});
|
|
226
|
-
setScaffold(s);
|
|
227
|
-
setResults((r) => ({ ...r, youtrack: s }));
|
|
228
|
-
}}
|
|
229
|
-
/>
|
|
230
|
-
{urlError && <Text color="red">{urlError}</Text>}
|
|
231
|
-
{scaffold ? (
|
|
232
|
-
<>
|
|
233
|
-
<Box flexDirection="column" gap={0}>
|
|
234
|
-
<Text color="green">Scaffolded {scaffold.youtrackJson}</Text>
|
|
235
|
-
<Text>Token placeholder: {scaffold.tokenPath}</Text>
|
|
236
|
-
<Text>Create token: {scaffold.tokenCreateUrl}</Text>
|
|
237
|
-
</Box>
|
|
238
|
-
{/* ponytail: @inkjs/ui has no focus system — render the ConfirmInput only once the scaffold
|
|
239
|
-
exists and disable the TextInput, so y reaches only the confirm (hint stays truthful) */}
|
|
240
|
-
<ConfirmInput
|
|
241
|
-
defaultChoice="confirm"
|
|
242
|
-
submitOnEnter={false}
|
|
243
|
-
onConfirm={onDone}
|
|
244
|
-
onCancel={() => {}}
|
|
245
|
-
/>
|
|
246
|
-
<Text dimColor>{continueLabel()}</Text>
|
|
247
|
-
</>
|
|
248
|
-
) : (
|
|
249
|
-
<Text dimColor>Enter to submit the URL — then y to continue</Text>
|
|
250
|
-
)}
|
|
251
|
-
</Box>
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function VcsStep({ results, setResults, onDone }: StepProps): JSX.Element {
|
|
256
|
-
const [provider, setProvider] = useState<VcsProvider>(results.vcs?.provider ?? "gitlab");
|
|
257
|
-
const [scaffold, setScaffold] = useState<VcsScaffold | null>(results.vcs);
|
|
258
|
-
|
|
259
|
-
const apply = (p: VcsProvider) => {
|
|
260
|
-
const s = scaffoldVcs(configDir(), p);
|
|
261
|
-
setScaffold(s);
|
|
262
|
-
setResults((r) => ({ ...r, vcs: s }));
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
// ponytail: Select's onChange only fires when Enter picks a different option — this useInput
|
|
266
|
-
// covers Enter on the default provider; scaffoldVcs is idempotent so double-apply is harmless
|
|
267
|
-
useInput((_input, key) => {
|
|
268
|
-
if (key.return) apply(provider);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
return (
|
|
272
|
-
<Box flexDirection="column" gap={1}>
|
|
273
|
-
<Text bold>Step 4 — Version control</Text>
|
|
274
|
-
<Text dimColor>Provider:</Text>
|
|
275
|
-
<Select
|
|
276
|
-
options={VCS_PROVIDERS}
|
|
277
|
-
defaultValue={provider}
|
|
278
|
-
onChange={(v) => {
|
|
279
|
-
const p = v as VcsProvider;
|
|
280
|
-
setProvider(p);
|
|
281
|
-
apply(p);
|
|
282
|
-
}}
|
|
283
|
-
/>
|
|
284
|
-
{scaffold ? (
|
|
285
|
-
<>
|
|
286
|
-
<Box flexDirection="column" gap={0}>
|
|
287
|
-
<Text color="green">Scaffolded {scaffold.vcsJson} (provider: {scaffold.provider})</Text>
|
|
288
|
-
<Text>Token placeholder: {scaffold.activeTokenPath}</Text>
|
|
289
|
-
<Text>Create token: {scaffold.tokenCreateUrl}</Text>
|
|
290
|
-
</Box>
|
|
291
|
-
<ConfirmInput
|
|
292
|
-
defaultChoice="confirm"
|
|
293
|
-
submitOnEnter={false}
|
|
294
|
-
onConfirm={onDone}
|
|
295
|
-
onCancel={() => {}}
|
|
296
|
-
/>
|
|
297
|
-
<Text dimColor>{continueLabel()}</Text>
|
|
298
|
-
</>
|
|
299
|
-
) : (
|
|
300
|
-
<Text dimColor>Enter to confirm the provider — then y to continue</Text>
|
|
301
|
-
)}
|
|
302
|
-
</Box>
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
type WsLinking = "youtrack" | "github" | "none";
|
|
307
|
-
|
|
308
|
-
type WsDraft = {
|
|
309
|
-
name: string;
|
|
310
|
-
glob: string;
|
|
311
|
-
provider: VcsProvider;
|
|
312
|
-
branch: string;
|
|
313
|
-
linking: WsLinking;
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
type WsMode = "list" | "name" | "glob" | "provider" | "branch" | "linking" | "remove";
|
|
317
|
-
|
|
318
|
-
// provider-gated linking: gitlab offers youtrack/none, github offers github-issues/none —
|
|
319
|
-
// config.sh gates issues on provider github, and an ungated youtrack link would leak
|
|
320
|
-
// "Related to: <youtrack>/issue/<id>" into GitHub PR bodies (writeWorkspaces enforces this too)
|
|
321
|
-
const WS_LINKING: Record<VcsProvider, { label: string; value: WsLinking }[]> = {
|
|
322
|
-
gitlab: [
|
|
323
|
-
{ label: "YouTrack", value: "youtrack" },
|
|
324
|
-
{ label: "None", value: "none" },
|
|
325
|
-
],
|
|
326
|
-
github: [
|
|
327
|
-
{ label: "GitHub issues", value: "github" },
|
|
328
|
-
{ label: "None", value: "none" },
|
|
329
|
-
],
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
// ponytail: Select has no onSubmit — onChange fires on Enter once the value differs from
|
|
333
|
-
// defaultValue, so action/provider/linking selects pass no defaultValue (undefined -> first
|
|
334
|
-
// Enter is a change). TextInput onSubmit fires on Enter even for empty input (validation).
|
|
335
|
-
// Each input gets a distinct key: mode swaps render the same element type at the same tree
|
|
336
|
-
// position, so without keys React reuses the instance and the previous input's text leaks in.
|
|
337
|
-
function WorkspacesStep({ setResults, onDone }: StepProps): JSX.Element {
|
|
338
|
-
const [loaded] = useState<WorkspaceConfig[]>(() => loadWorkspaces());
|
|
339
|
-
const [entries, setEntries] = useState<WorkspaceConfig[]>(loaded);
|
|
340
|
-
const [mode, setMode] = useState<WsMode>("list");
|
|
341
|
-
const [draft, setDraft] = useState<WsDraft>({ name: "", glob: "", provider: "gitlab", branch: "develop", linking: "none" });
|
|
342
|
-
const [fieldError, setFieldError] = useState<string | null>(null);
|
|
343
|
-
const [writeError, setWriteError] = useState<string | null>(null);
|
|
344
|
-
|
|
345
|
-
const resetDraft = () => setDraft({ name: "", glob: "", provider: "gitlab", branch: "develop", linking: "none" });
|
|
346
|
-
|
|
347
|
-
const finish = () => {
|
|
348
|
-
setResults((r) => ({ ...r, workspaces: entries }));
|
|
349
|
-
if (!shouldWriteWorkspaces(loaded, entries)) {
|
|
350
|
-
onDone();
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
const result = writeWorkspaces(entries);
|
|
354
|
-
if (result.ok) {
|
|
355
|
-
onDone();
|
|
356
|
-
} else {
|
|
357
|
-
setWriteError(result.error ?? "failed to write workspaces.json");
|
|
358
|
-
setMode("list");
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
// ponytail: @inkjs/ui v2 Select options have no per-option isDisabled (whole Select only,
|
|
363
|
-
// which would also block Done) — the empty-list guard stays in the onChange instead
|
|
364
|
-
const actions = [
|
|
365
|
-
{ label: "Add workspace", value: "add" },
|
|
366
|
-
{ label: "Remove workspace", value: "remove" },
|
|
367
|
-
{ label: "Done", value: "done" },
|
|
368
|
-
];
|
|
369
|
-
|
|
370
|
-
if (mode === "list") {
|
|
371
|
-
return (
|
|
372
|
-
<Box flexDirection="column" gap={1}>
|
|
373
|
-
<Text bold>Step 5 — Workspaces</Text>
|
|
374
|
-
{entries.length === 0 && <Text dimColor>No workspaces configured yet.</Text>}
|
|
375
|
-
{entries.map((e) => (
|
|
376
|
-
<Text key={`${e.name}|${e.glob}|${e.vcs?.provider ?? ""}`}>
|
|
377
|
-
• {e.name} — {e.vcs?.provider ?? "?"} — {e.glob}
|
|
378
|
-
</Text>
|
|
379
|
-
))}
|
|
380
|
-
<Text dimColor>Select an action:</Text>
|
|
381
|
-
<Select
|
|
382
|
-
key="actions"
|
|
383
|
-
options={actions}
|
|
384
|
-
onChange={(v) => {
|
|
385
|
-
if (v === "add") {
|
|
386
|
-
setFieldError(null);
|
|
387
|
-
setWriteError(null);
|
|
388
|
-
setMode("name");
|
|
389
|
-
} else if (v === "remove" && entries.length > 0) {
|
|
390
|
-
setFieldError(null);
|
|
391
|
-
setWriteError(null);
|
|
392
|
-
setMode("remove");
|
|
393
|
-
} else if (v === "done") {
|
|
394
|
-
finish();
|
|
395
|
-
}
|
|
396
|
-
}}
|
|
397
|
-
/>
|
|
398
|
-
{writeError && <Text color="red">{writeError}</Text>}
|
|
399
|
-
<Text dimColor>Enter to pick · Esc to exit</Text>
|
|
400
|
-
</Box>
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (mode === "name") {
|
|
405
|
-
return (
|
|
406
|
-
<Box flexDirection="column" gap={1}>
|
|
407
|
-
<Text bold>Step 5 — Workspaces · new workspace</Text>
|
|
408
|
-
<Text dimColor>Name (e.g. work):</Text>
|
|
409
|
-
<TextInput
|
|
410
|
-
key="name"
|
|
411
|
-
onSubmit={(v) => {
|
|
412
|
-
const name = v.trim();
|
|
413
|
-
if (!name) {
|
|
414
|
-
setFieldError("name is required");
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
if (entries.some((e) => e.name === name)) {
|
|
418
|
-
setFieldError(`"${name}" already exists — pick a unique name`);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
setFieldError(null);
|
|
422
|
-
setDraft({ ...draft, name });
|
|
423
|
-
setMode("glob");
|
|
424
|
-
}}
|
|
425
|
-
/>
|
|
426
|
-
{fieldError && <Text color="red">{fieldError}</Text>}
|
|
427
|
-
</Box>
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (mode === "glob") {
|
|
432
|
-
return (
|
|
433
|
-
<Box flexDirection="column" gap={1}>
|
|
434
|
-
<Text bold>Step 5 — Workspaces · {draft.name}</Text>
|
|
435
|
-
<Text dimColor>Path glob (e.g. /home/*/Documents/projects/work/**):</Text>
|
|
436
|
-
<TextInput
|
|
437
|
-
key="glob"
|
|
438
|
-
onSubmit={(v) => {
|
|
439
|
-
if (!v.trim()) {
|
|
440
|
-
setFieldError("glob is required");
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
setFieldError(null);
|
|
444
|
-
setDraft({ ...draft, glob: v.trim() });
|
|
445
|
-
setMode("provider");
|
|
446
|
-
}}
|
|
447
|
-
/>
|
|
448
|
-
{fieldError && <Text color="red">{fieldError}</Text>}
|
|
449
|
-
</Box>
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (mode === "provider") {
|
|
454
|
-
return (
|
|
455
|
-
<Box flexDirection="column" gap={1}>
|
|
456
|
-
<Text bold>Step 5 — Workspaces · {draft.name}</Text>
|
|
457
|
-
<Text dimColor>VCS provider:</Text>
|
|
458
|
-
<Select
|
|
459
|
-
key="provider"
|
|
460
|
-
options={VCS_PROVIDERS}
|
|
461
|
-
onChange={(v) => {
|
|
462
|
-
const p = v as VcsProvider;
|
|
463
|
-
setDraft({ ...draft, provider: p, branch: p === "gitlab" ? "develop" : "main" });
|
|
464
|
-
setMode("branch");
|
|
465
|
-
}}
|
|
466
|
-
/>
|
|
467
|
-
</Box>
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (mode === "branch") {
|
|
472
|
-
return (
|
|
473
|
-
<Box flexDirection="column" gap={1}>
|
|
474
|
-
<Text bold>Step 5 — Workspaces · {draft.name}</Text>
|
|
475
|
-
<Text dimColor>Default target branch (Enter to keep "{draft.branch}"):</Text>
|
|
476
|
-
<TextInput
|
|
477
|
-
key="branch"
|
|
478
|
-
defaultValue={draft.branch}
|
|
479
|
-
onSubmit={(v) => {
|
|
480
|
-
setDraft({ ...draft, branch: v.trim() });
|
|
481
|
-
setMode("linking");
|
|
482
|
-
}}
|
|
483
|
-
/>
|
|
484
|
-
</Box>
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (mode === "linking") {
|
|
489
|
-
return (
|
|
490
|
-
<Box flexDirection="column" gap={1}>
|
|
491
|
-
<Text bold>Step 5 — Workspaces · {draft.name}</Text>
|
|
492
|
-
<Text dimColor>Issue linking:</Text>
|
|
493
|
-
<Select
|
|
494
|
-
key="linking"
|
|
495
|
-
options={WS_LINKING[draft.provider]}
|
|
496
|
-
onChange={(v) => {
|
|
497
|
-
const linking = v as WsLinking;
|
|
498
|
-
const vcs = { provider: draft.provider, ...(draft.branch ? { defaultTargetBranch: draft.branch } : {}) };
|
|
499
|
-
setEntries([
|
|
500
|
-
...entries,
|
|
501
|
-
{
|
|
502
|
-
name: draft.name,
|
|
503
|
-
glob: draft.glob,
|
|
504
|
-
vcs,
|
|
505
|
-
...(linking === "youtrack" ? { youtrack: { link_issues: true } } : {}),
|
|
506
|
-
...(linking === "github" ? { issues: { provider: "github", link_on_pr: true } } : {}),
|
|
507
|
-
},
|
|
508
|
-
]);
|
|
509
|
-
resetDraft();
|
|
510
|
-
setWriteError(null);
|
|
511
|
-
setMode("list");
|
|
512
|
-
}}
|
|
513
|
-
/>
|
|
514
|
-
</Box>
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return (
|
|
519
|
-
<Box flexDirection="column" gap={1}>
|
|
520
|
-
<Text bold>Step 5 — Workspaces · remove</Text>
|
|
521
|
-
<Text dimColor>Select a workspace to remove:</Text>
|
|
522
|
-
<Select
|
|
523
|
-
key="remove"
|
|
524
|
-
options={entries.map((e) => ({ label: `${e.name} — ${e.vcs?.provider ?? "?"}`, value: e.name }))}
|
|
525
|
-
onChange={(v) => {
|
|
526
|
-
setEntries(entries.filter((e) => e.name !== v));
|
|
527
|
-
setWriteError(null);
|
|
528
|
-
setMode("list");
|
|
529
|
-
}}
|
|
530
|
-
/>
|
|
531
|
-
</Box>
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function ProjectStep({ setResults, onDone }: StepProps): JSX.Element {
|
|
536
|
-
const apply = () => {
|
|
537
|
-
const result = runProjectSetup(process.cwd());
|
|
538
|
-
setResults((r) => ({ ...r, project: result }));
|
|
539
|
-
onDone();
|
|
540
|
-
};
|
|
541
|
-
|
|
542
|
-
return (
|
|
543
|
-
<Box flexDirection="column" gap={1}>
|
|
544
|
-
<Text bold>Step 6 — Project setup</Text>
|
|
545
|
-
<Text dimColor>Will apply gitignore + hygiene in {process.cwd()} (existing files are never overwritten):</Text>
|
|
546
|
-
<ConfirmInput defaultChoice="confirm" submitOnEnter={false} onConfirm={apply} onCancel={() => {}} />
|
|
547
|
-
<Text dimColor>{continueLabel()}</Text>
|
|
548
|
-
</Box>
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function SummaryStep({ results, onExit }: StepProps): JSX.Element {
|
|
553
|
-
return (
|
|
554
|
-
<Box flexDirection="column" gap={1}>
|
|
555
|
-
<Text bold color="cyan">
|
|
556
|
-
Setup complete
|
|
557
|
-
</Text>
|
|
558
|
-
<Text>
|
|
559
|
-
Platforms: <Text color="green">{results.platforms.join(", ")}</Text>
|
|
560
|
-
</Text>
|
|
561
|
-
<Text>
|
|
562
|
-
Global config: <Text color="green">{configDir()}/config.json</Text>
|
|
563
|
-
</Text>
|
|
564
|
-
{results.youtrack && (
|
|
565
|
-
<Box flexDirection="column" gap={0}>
|
|
566
|
-
<Text>
|
|
567
|
-
YouTrack: <Text color="green">{results.youtrack.youtrackJson}</Text>
|
|
568
|
-
</Text>
|
|
569
|
-
<Text> token placeholder: {results.youtrack.tokenPath}</Text>
|
|
570
|
-
<Text> create token: {results.youtrack.tokenCreateUrl}</Text>
|
|
571
|
-
</Box>
|
|
572
|
-
)}
|
|
573
|
-
{results.vcs && (
|
|
574
|
-
<Box flexDirection="column" gap={0}>
|
|
575
|
-
<Text>
|
|
576
|
-
VCS: <Text color="green">{results.vcs.vcsJson}</Text> (provider: {results.vcs.provider})
|
|
577
|
-
</Text>
|
|
578
|
-
<Text> token placeholder: {results.vcs.activeTokenPath}</Text>
|
|
579
|
-
<Text> create token: {results.vcs.tokenCreateUrl}</Text>
|
|
580
|
-
</Box>
|
|
581
|
-
)}
|
|
582
|
-
{results.workspaces.length > 0 && (
|
|
583
|
-
<Box flexDirection="column" gap={0}>
|
|
584
|
-
<Text>Workspaces:</Text>
|
|
585
|
-
{results.workspaces.map((w) => (
|
|
586
|
-
<Text key={`${w.name}|${w.glob}|${w.vcs?.provider ?? ""}`}>
|
|
587
|
-
{" "}{w.name} — {w.vcs?.provider ?? "?"}
|
|
588
|
-
</Text>
|
|
589
|
-
))}
|
|
590
|
-
</Box>
|
|
591
|
-
)}
|
|
592
|
-
{results.project && results.project.created.length > 0 && (
|
|
593
|
-
<Box flexDirection="column" gap={0}>
|
|
594
|
-
<Text>Project files:</Text>
|
|
595
|
-
{results.project.created.map((file) => (
|
|
596
|
-
<Text key={file}> + {file}</Text>
|
|
597
|
-
))}
|
|
598
|
-
</Box>
|
|
599
|
-
)}
|
|
600
|
-
<Text dimColor>Paste the token(s) into the placeholder files, then run /wf-status to verify.</Text>
|
|
601
|
-
<ConfirmInput defaultChoice="confirm" submitOnEnter={false} onConfirm={onExit} onCancel={() => {}} />
|
|
602
|
-
<Text dimColor>{continueLabel()}</Text>
|
|
603
|
-
</Box>
|
|
604
|
-
);
|
|
605
|
-
}
|