@brainervirus/workit-cli 0.5.1 → 0.5.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainervirus/workit-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Workit interactive setup wizard (Ink TUI)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,21 +12,23 @@
|
|
|
12
12
|
"setup"
|
|
13
13
|
],
|
|
14
14
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/BrainerVirus/
|
|
15
|
+
"url": "https://github.com/BrainerVirus/workit/issues"
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "https://github.com/BrainerVirus/
|
|
19
|
+
"url": "https://github.com/BrainerVirus/workit.git"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
|
-
"
|
|
22
|
+
"dist/"
|
|
23
23
|
],
|
|
24
24
|
"type": "module",
|
|
25
25
|
"bin": {
|
|
26
|
-
"workit": "./
|
|
26
|
+
"workit": "./dist/index.js"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"
|
|
29
|
+
"build": "bun build ./src/index.tsx --outdir dist --target node --format esm --banner \"#!/usr/bin/env node\" --external ink --external @inkjs/ui --external react",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"prepublishOnly": "bun run build && bun run typecheck"
|
|
30
32
|
},
|
|
31
33
|
"publishConfig": {
|
|
32
34
|
"access": "public"
|
package/src/index.tsx
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { render } from "ink";
|
|
3
|
-
import { Wizard } from "./steps";
|
|
4
|
-
|
|
5
|
-
const HELP = `workit — workflow rails for agentic coding
|
|
6
|
-
|
|
7
|
-
Usage:
|
|
8
|
-
workit init Run the interactive setup wizard
|
|
9
|
-
workit Show this help
|
|
10
|
-
|
|
11
|
-
Run \`npx workit init\` to configure platforms, YouTrack, VCS and project hygiene.
|
|
12
|
-
`;
|
|
13
|
-
|
|
14
|
-
async function runInit() {
|
|
15
|
-
// ponytail: no-TTY guard — piping/disabling stdin would hang render(); print and exit cleanly
|
|
16
|
-
if (process.stdin.isTTY !== true) {
|
|
17
|
-
console.log("workit init requires an interactive terminal (TTY).");
|
|
18
|
-
process.exit(0);
|
|
19
|
-
}
|
|
20
|
-
let done: () => void = () => {};
|
|
21
|
-
const { waitUntilExit, unmount } = render(<Wizard onExit={() => done()} />);
|
|
22
|
-
done = unmount;
|
|
23
|
-
await waitUntilExit();
|
|
24
|
-
process.exit(0);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (import.meta.main) {
|
|
28
|
-
const [subcommand] = process.argv.slice(2);
|
|
29
|
-
if (subcommand === "init") {
|
|
30
|
-
await runInit();
|
|
31
|
-
} else {
|
|
32
|
-
console.log(HELP);
|
|
33
|
-
process.exit(0);
|
|
34
|
-
}
|
|
35
|
-
}
|
package/src/logic.ts
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { PRESETS, LOCALE_RE, type BranchPreset, type ToolkitConfig } from "@brainervirus/workit-core/src/core/config.ts";
|
|
5
|
-
import { workspacesPath, type WorkspaceConfig } from "@brainervirus/workit-core/src/core/workspaces.ts";
|
|
6
|
-
import { ensureProjectGitignore } from "@brainervirus/workit-core/src/core/gitignore.ts";
|
|
7
|
-
import { ensureHygieneFiles, hygieneFiles } from "@brainervirus/workit-core/src/core/hygiene.ts";
|
|
8
|
-
|
|
9
|
-
export const TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
10
|
-
|
|
11
|
-
export function validateLocale(locale: string): string | null {
|
|
12
|
-
if (!LOCALE_RE.test(locale)) {
|
|
13
|
-
return `invalid locale "${locale}" — expected BCP-47 like en or es-CL`;
|
|
14
|
-
}
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const KNOWN_TIMEZONES: string[] | null =
|
|
19
|
-
typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("timeZone") : null;
|
|
20
|
-
|
|
21
|
-
export function validateTimezone(timezone: string): string | null {
|
|
22
|
-
const tz = timezone.trim();
|
|
23
|
-
if (!tz) return "timezone is required";
|
|
24
|
-
if (KNOWN_TIMEZONES && !KNOWN_TIMEZONES.includes(tz)) {
|
|
25
|
-
return `unknown timezone "${tz}" — check the IANA name (e.g. America/Santiago)`;
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function validateBaseUrl(url: string): string | null {
|
|
31
|
-
let parsed: URL;
|
|
32
|
-
try {
|
|
33
|
-
parsed = new URL(url.trim());
|
|
34
|
-
} catch {
|
|
35
|
-
return `invalid URL "${url}"`;
|
|
36
|
-
}
|
|
37
|
-
if (parsed.protocol !== "https:") return "base URL must use https";
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function parseList(raw: string): string[] {
|
|
42
|
-
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type ConfigInput = {
|
|
46
|
-
locale?: string;
|
|
47
|
-
timezone?: string;
|
|
48
|
-
preset?: BranchPreset;
|
|
49
|
-
allowed?: string[];
|
|
50
|
-
protectedNames?: string[];
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export function collectConfigValues(input: ConfigInput, current: ToolkitConfig): ToolkitConfig {
|
|
54
|
-
const preset = input.preset ?? current.branchPolicy.preset;
|
|
55
|
-
const presetDefs = PRESETS[preset];
|
|
56
|
-
return {
|
|
57
|
-
locale: input.locale ?? current.locale,
|
|
58
|
-
localeOptions: current.localeOptions,
|
|
59
|
-
timezone: input.timezone ?? current.timezone,
|
|
60
|
-
branchPolicy: {
|
|
61
|
-
preset,
|
|
62
|
-
allowed: preset === "custom" ? (input.allowed ?? current.branchPolicy.allowed) : [...presetDefs.allowed],
|
|
63
|
-
protected: preset === "custom" ? (input.protectedNames ?? current.branchPolicy.protected) : [...presetDefs.protected],
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export type ProjectSetupResult = {
|
|
69
|
-
gitignore: { ok: true; path: string; added: string[] } | { ok: false; error: string };
|
|
70
|
-
hygiene: { ok: true; created: string[] } | { ok: false; error: string };
|
|
71
|
-
openSource: boolean;
|
|
72
|
-
created: string[];
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
export function runProjectSetup(root: string, opts: { includeOpenSource?: boolean } = {}): ProjectSetupResult {
|
|
76
|
-
const openSource = opts.includeOpenSource ?? hygieneFiles(root).openSource;
|
|
77
|
-
const gitignore = ensureProjectGitignore(root, true);
|
|
78
|
-
const hygiene = ensureHygieneFiles(root, { confirmed: true, includeOpenSource: openSource });
|
|
79
|
-
const created = [
|
|
80
|
-
...(gitignore.ok ? gitignore.added : []),
|
|
81
|
-
...(hygiene.ok ? hygiene.created : []),
|
|
82
|
-
];
|
|
83
|
-
return { gitignore, hygiene, openSource, created };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export const DEFAULT_BASE_URL = "https://enghouseamg.youtrack.cloud";
|
|
87
|
-
|
|
88
|
-
export type YouTrackScaffold = {
|
|
89
|
-
youtrackJson: string;
|
|
90
|
-
tokenPath: string;
|
|
91
|
-
tokenCreateUrl: string;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// ponytail: mirrors scripts/init/apply.sh write_youtrack_json + write_token_placeholder +
|
|
95
|
-
// scripts/youtrack/token-create-url.sh in TS — initApply shells out to bash (CA-01 forbids bash)
|
|
96
|
-
// ponytail: mirrors apply.sh; WORKFLOW_YT_*/WORKFLOW_VCS_* env overrides intentionally ignored
|
|
97
|
-
// (wizard takes values from prompts instead of env; parity pinned by test/scaffold-parity.test.ts)
|
|
98
|
-
export function scaffoldYouTrack(dir: string, baseUrl: string, opts: { locale?: string; timezone?: string } = {}): YouTrackScaffold {
|
|
99
|
-
mkdirSync(dir, { recursive: true });
|
|
100
|
-
const youtrackJson = path.join(dir, "youtrack.json");
|
|
101
|
-
const tokenPath = path.join(dir, "youtrack.token");
|
|
102
|
-
const config = {
|
|
103
|
-
baseUrl,
|
|
104
|
-
tokenFile: tokenPath,
|
|
105
|
-
timezone: opts.timezone ?? "America/Santiago",
|
|
106
|
-
locale: opts.locale ?? "es-CL",
|
|
107
|
-
defaultMention: "Alejandra.Flores",
|
|
108
|
-
greetings: { morning: "buenos días", afternoon: "buenas tardes" },
|
|
109
|
-
greetingCutoff: "12:00",
|
|
110
|
-
meetingIssue: "IRPT-12",
|
|
111
|
-
meetingIssues: {
|
|
112
|
-
general: {
|
|
113
|
-
issue: "IRPT-12",
|
|
114
|
-
label: "General meetings (Reuniones internas Team IRP)",
|
|
115
|
-
workItemText: "Reuniones",
|
|
116
|
-
},
|
|
117
|
-
web: {
|
|
118
|
-
issue: "NSXFT-21",
|
|
119
|
-
label: "Web meetings",
|
|
120
|
-
workItemText: "Reuniones web",
|
|
121
|
-
url: "https://enghouseamg.youtrack.cloud/projects/NSXFT/issues/NSXFT-21",
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
commentHeader: "# Actualización",
|
|
125
|
-
attachmentsHeaderImages: "## Adjunto capturas",
|
|
126
|
-
attachmentsHeaderFiles: "## Archivos adjuntos",
|
|
127
|
-
attachmentsHeaderMixed: "## Adjuntos",
|
|
128
|
-
tokenDefaults: {
|
|
129
|
-
name: "workit",
|
|
130
|
-
description: "OpenCode workit — /wk-issue-update and /wk-meetings",
|
|
131
|
-
scopes: ["YouTrack"],
|
|
132
|
-
profileTab: "account-security",
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
writeFileSync(youtrackJson, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
136
|
-
writeFileSync(tokenPath, TOKEN_PLACEHOLDER + "\n", { encoding: "utf8", mode: 0o600 });
|
|
137
|
-
const base = baseUrl.replace(/\/+$/, "");
|
|
138
|
-
return {
|
|
139
|
-
youtrackJson,
|
|
140
|
-
tokenPath,
|
|
141
|
-
tokenCreateUrl: `${base}/users/me?tab=account-security`,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export type VcsProvider = "gitlab" | "github";
|
|
146
|
-
|
|
147
|
-
export type VcsScaffold = {
|
|
148
|
-
vcsJson: string;
|
|
149
|
-
tokenPaths: string[];
|
|
150
|
-
activeTokenPath: string;
|
|
151
|
-
tokenCreateUrl: string;
|
|
152
|
-
provider: VcsProvider;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const TOKEN_DEFAULTS = {
|
|
156
|
-
name: "workit",
|
|
157
|
-
description: "OpenCode workit — /wk-pr and glab/gh",
|
|
158
|
-
gitlabScopes: ["api"],
|
|
159
|
-
githubPermissions: { pull_requests: "write", contents: "write", metadata: "read" },
|
|
160
|
-
githubClassicScopes: ["repo"],
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
// ponytail: mirrors scripts/init/apply.sh write_vcs_json + write_vcs_token_placeholder +
|
|
164
|
-
// scripts/vcs/token-create-urls.sh in TS — same no-bash rationale as scaffoldYouTrack
|
|
165
|
-
export function scaffoldVcs(dir: string, provider: VcsProvider): VcsScaffold {
|
|
166
|
-
mkdirSync(dir, { recursive: true });
|
|
167
|
-
const vcsJson = path.join(dir, "vcs.json");
|
|
168
|
-
const gitlabToken = path.join(dir, "gitlab.token");
|
|
169
|
-
const githubToken = path.join(dir, "github.token");
|
|
170
|
-
const config = {
|
|
171
|
-
provider,
|
|
172
|
-
defaultTargetBranch: "develop",
|
|
173
|
-
gitlab: { host: "gitlab.com", apiUrl: "https://gitlab.com/api/v4", tokenFile: gitlabToken },
|
|
174
|
-
github: { host: "github.com", tokenFile: githubToken },
|
|
175
|
-
pr: { squashOnMerge: true, removeSourceBranch: true, pushBranch: true, confirmSkip: true },
|
|
176
|
-
tokenDefaults: TOKEN_DEFAULTS,
|
|
177
|
-
};
|
|
178
|
-
writeFileSync(vcsJson, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
179
|
-
for (const p of [gitlabToken, githubToken]) {
|
|
180
|
-
writeFileSync(p, TOKEN_PLACEHOLDER + "\n", { encoding: "utf8", mode: 0o600 });
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const gitlabUrl = `https://gitlab.com/-/user_settings/personal_access_tokens?${new URLSearchParams({
|
|
184
|
-
name: TOKEN_DEFAULTS.name,
|
|
185
|
-
description: TOKEN_DEFAULTS.description,
|
|
186
|
-
scopes: "api",
|
|
187
|
-
})}`;
|
|
188
|
-
const githubUrl = `https://github.com/settings/personal-access-tokens/new?${new URLSearchParams({
|
|
189
|
-
name: TOKEN_DEFAULTS.name,
|
|
190
|
-
description: TOKEN_DEFAULTS.description,
|
|
191
|
-
pull_requests: "write",
|
|
192
|
-
contents: "write",
|
|
193
|
-
metadata: "read",
|
|
194
|
-
})}`;
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
vcsJson,
|
|
198
|
-
tokenPaths: [gitlabToken, githubToken],
|
|
199
|
-
activeTokenPath: provider === "gitlab" ? gitlabToken : githubToken,
|
|
200
|
-
tokenCreateUrl: provider === "gitlab" ? gitlabUrl : githubUrl,
|
|
201
|
-
provider,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function shouldWriteWorkspaces(loaded: WorkspaceConfig[], current: WorkspaceConfig[]): boolean {
|
|
206
|
-
return !isDeepStrictEqual(loaded, current);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function loadWorkspaces(): WorkspaceConfig[] {
|
|
210
|
-
let raw: string;
|
|
211
|
-
try {
|
|
212
|
-
raw = readFileSync(workspacesPath(), "utf8");
|
|
213
|
-
} catch {
|
|
214
|
-
return [];
|
|
215
|
-
}
|
|
216
|
-
let parsed: unknown;
|
|
217
|
-
try {
|
|
218
|
-
parsed = JSON.parse(raw);
|
|
219
|
-
} catch {
|
|
220
|
-
return [];
|
|
221
|
-
}
|
|
222
|
-
if (!parsed || typeof parsed !== "object") return [];
|
|
223
|
-
const list = (parsed as { workspaces?: unknown }).workspaces;
|
|
224
|
-
return Array.isArray(list) ? (list as WorkspaceConfig[]) : [];
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export type WriteWorkspacesResult = { ok: boolean; error?: string; path: string };
|
|
228
|
-
|
|
229
|
-
const VALID_PROVIDERS: VcsProvider[] = ["gitlab", "github"];
|
|
230
|
-
|
|
231
|
-
export function writeWorkspaces(entries: WorkspaceConfig[]): WriteWorkspacesResult {
|
|
232
|
-
const file = workspacesPath();
|
|
233
|
-
for (const [i, entry] of entries.entries()) {
|
|
234
|
-
if (!entry || typeof entry !== "object") {
|
|
235
|
-
return { ok: false, error: `workspace #${i + 1} is null`, path: file };
|
|
236
|
-
}
|
|
237
|
-
if (typeof entry.name !== "string" || !entry.name.trim()) {
|
|
238
|
-
return { ok: false, error: `workspace #${i + 1} missing a name`, path: file };
|
|
239
|
-
}
|
|
240
|
-
if (typeof entry.glob !== "string" || !entry.glob.trim()) {
|
|
241
|
-
return { ok: false, error: `workspace "${entry.name}" missing a glob`, path: file };
|
|
242
|
-
}
|
|
243
|
-
const provider = entry.vcs?.provider;
|
|
244
|
-
if (provider && !VALID_PROVIDERS.includes(provider)) {
|
|
245
|
-
return { ok: false, error: `workspace "${entry.name}" has unknown provider "${provider}"`, path: file };
|
|
246
|
-
}
|
|
247
|
-
if (entry.youtrack && provider !== "gitlab") {
|
|
248
|
-
return { ok: false, error: `workspace "${entry.name}" links YouTrack issues but provider is "${provider ?? "unset"}" — youtrack linking requires the gitlab provider`, path: file };
|
|
249
|
-
}
|
|
250
|
-
if (entry.issues && provider !== "github") {
|
|
251
|
-
return { ok: false, error: `workspace "${entry.name}" links GitHub issues but provider is "${provider ?? "unset"}" — github issues require the github provider`, path: file };
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
mkdirSync(path.dirname(file), { recursive: true });
|
|
255
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
256
|
-
try {
|
|
257
|
-
writeFileSync(tmp, JSON.stringify({ workspaces: entries }, null, 2) + "\n", "utf8");
|
|
258
|
-
renameSync(tmp, file);
|
|
259
|
-
} catch (err) {
|
|
260
|
-
return { ok: false, error: `failed to write ${file}: ${(err as Error).message}`, path: file };
|
|
261
|
-
}
|
|
262
|
-
return { ok: true, path: file };
|
|
263
|
-
}
|