komplian 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/komplian-onboard.mjs +503 -0
- package/komplian-team-repos.json +14 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# komplian (npm CLI)
|
|
2
|
+
|
|
3
|
+
**Developers:** from any machine with Node 18+ and GitHub CLI (`gh`):
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx komplian onboard --yes
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
This downloads the CLI from npm (no manual git clone), opens GitHub login if needed, and clones the repos your account can access (filtered by team in the bundled `komplian-team-repos.json`). Default workspace: `~/komplian` (Windows: `%USERPROFILE%\komplian`).
|
|
10
|
+
|
|
11
|
+
**Maintainers:** publish **from this folder** (the one that contains `package.json` — not the monorepo root):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd path/to/monorepo/scripts # e.g. cd ~/Desktop/temp/scripts
|
|
15
|
+
npm login # once per machine
|
|
16
|
+
npm publish --access public
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Running `npm publish` from the repo root causes `ENOENT package.json` because the npm package lives only under `scripts/`.
|
|
20
|
+
|
|
21
|
+
Bump `version` in `package.json` before each publish.
|
|
22
|
+
|
|
23
|
+
**If publish fails with `403` / “Two-factor authentication … required”:** enable **2FA** on your npm account (*npmjs.com → Account → Security*), then run `npm publish` again (npm may prompt for an OTP). For CI, create a **granular access token** (*Access Tokens → Granular token*) with **Publish** on the `komplian` package and “**Bypass two-factor authentication**” if your org allows it; set `NPM_TOKEN` in GitHub Actions.
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Komplian onboard — one-command dev setup (GitHub CLI auth + clone + optional npm install).
|
|
4
|
+
* Zero npm deps: Node 18+, git, gh, and komplian-team-repos.json beside this file.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx komplian onboard --yes # npm ships this package; no manual monorepo clone for developers
|
|
8
|
+
*
|
|
9
|
+
* Security: `gh` OAuth; active org membership verified via GitHub API before clones; repo ACLs enforced by GitHub. See ONBOARDING.md.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { existsSync, readFileSync, mkdirSync, cpSync, rmSync, readdirSync, statSync } from "node:fs";
|
|
14
|
+
import { dirname, join, resolve, normalize } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { createInterface } from "node:readline/promises";
|
|
17
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
const c = {
|
|
23
|
+
reset: "\x1b[0m",
|
|
24
|
+
dim: "\x1b[2m",
|
|
25
|
+
bold: "\x1b[1m",
|
|
26
|
+
cyan: "\x1b[36m",
|
|
27
|
+
green: "\x1b[32m",
|
|
28
|
+
red: "\x1b[31m",
|
|
29
|
+
yellow: "\x1b[33m",
|
|
30
|
+
blue: "\x1b[34m",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function log(s = "") {
|
|
34
|
+
console.log(s);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function banner() {
|
|
38
|
+
// Block Unicode (cfonts "block"). Fourth letter is P: row 4 uses ██╔═══╝; R would use ██╔══██╗.
|
|
39
|
+
log(
|
|
40
|
+
[
|
|
41
|
+
`${c.cyan}${c.bold}`,
|
|
42
|
+
"██╗ ██╗ ██████╗ ███╗ ███╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗",
|
|
43
|
+
"██║ ██╔╝ ██╔═══██╗ ████╗ ████║ ██╔══██╗ ██║ ██║ ██╔══██╗ ████╗ ██║",
|
|
44
|
+
"█████╔╝ ██║ ██║ ██╔████╔██║ ██████╔╝ ██║ ██║ ███████║ ██╔██╗ ██║",
|
|
45
|
+
"██╔═██╗ ██║ ██║ ██║╚██╔╝██║ ██╔═══╝ ██║ ██║ ██╔══██║ ██║╚██╗██║",
|
|
46
|
+
"██║ ██╗ ╚██████╔╝ ██║ ╚═╝ ██║ ██║ ███████╗ ██║ ██║ ██║ ██║ ╚████║",
|
|
47
|
+
"╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝",
|
|
48
|
+
`${c.reset}`,
|
|
49
|
+
`${c.dim} Secure setup · GitHub CLI · HTTPS clones · No secrets stored by this CLI${c.reset}`,
|
|
50
|
+
"",
|
|
51
|
+
].join("\n")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ghOk() {
|
|
56
|
+
const r = spawnSync("gh", ["auth", "status", "-h", "github.com"], {
|
|
57
|
+
encoding: "utf8",
|
|
58
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
59
|
+
});
|
|
60
|
+
return r.status === 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Scopes: repo (clone), read:org (verify org membership — required for private orgs). */
|
|
64
|
+
function runGhAuth() {
|
|
65
|
+
log(
|
|
66
|
+
`${c.yellow}→${c.reset} Opening ${c.bold}GitHub${c.reset} login in the browser (OAuth). We never see your password.`
|
|
67
|
+
);
|
|
68
|
+
const r = spawnSync(
|
|
69
|
+
"gh",
|
|
70
|
+
["auth", "login", "-h", "github.com", "-s", "repo", "-s", "read:org", "-w"],
|
|
71
|
+
{ stdio: "inherit" }
|
|
72
|
+
);
|
|
73
|
+
return r.status === 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ghApiJson(path) {
|
|
77
|
+
const r = spawnSync("gh", ["api", path], {
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
80
|
+
});
|
|
81
|
+
return { status: r.status, stdout: r.stdout || "", stderr: r.stderr || "" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Ensures the authenticated user is an active member of the GitHub org (not just any GitHub account).
|
|
86
|
+
* Relies on GitHub as source of truth; optional SAML/SSO is enforced by the org on github.com.
|
|
87
|
+
*/
|
|
88
|
+
function verifyOrgMembership(org) {
|
|
89
|
+
const enc = encodeURIComponent(org);
|
|
90
|
+
const mem = ghApiJson(`user/memberships/orgs/${enc}`);
|
|
91
|
+
if (mem.status === 200) {
|
|
92
|
+
try {
|
|
93
|
+
const j = JSON.parse(mem.stdout);
|
|
94
|
+
if (j.state === "active") {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
log(
|
|
98
|
+
`${c.red}✗${c.reset} Organization ${c.bold}${org}${c.reset} membership is not active (state: ${j.state || "?"}).`
|
|
99
|
+
);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
} catch {
|
|
102
|
+
log(`${c.red}✗${c.reset} Invalid response verifying org membership.`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (mem.status === 404) {
|
|
107
|
+
log(
|
|
108
|
+
`${c.red}✗${c.reset} This GitHub account is ${c.bold}not a member${c.reset} of org ${c.bold}${org}${c.reset}.`
|
|
109
|
+
);
|
|
110
|
+
log(
|
|
111
|
+
`${c.dim} Only invited org members can run this tool. Wrong account? Run: gh auth logout && gh auth login${c.reset}`
|
|
112
|
+
);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const hint = (mem.stderr + mem.stdout).toLowerCase();
|
|
116
|
+
if (mem.status === 403 || hint.includes("read:org") || hint.includes("scope")) {
|
|
117
|
+
log(`${c.red}✗${c.reset} Token cannot verify org membership (needs ${c.bold}read:org${c.reset} scope).`);
|
|
118
|
+
log(
|
|
119
|
+
`${c.dim} Run: gh auth refresh -h github.com -s repo -s read:org${c.reset}`
|
|
120
|
+
);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
log(
|
|
124
|
+
`${c.red}✗${c.reset} Could not verify org membership (${mem.status}):\n${c.dim}${(mem.stderr || mem.stdout).trim()}${c.reset}`
|
|
125
|
+
);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function logGhIdentity() {
|
|
130
|
+
const u = ghApiJson("user");
|
|
131
|
+
if (u.status !== 200) return;
|
|
132
|
+
try {
|
|
133
|
+
const j = JSON.parse(u.stdout);
|
|
134
|
+
if (j.login) {
|
|
135
|
+
log(`${c.dim} Signed in as ${c.bold}@${j.login}${c.reset}${c.dim} (github.com)${c.reset}`);
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
/* ignore */
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function needCmd(name) {
|
|
143
|
+
const r = spawnSync("command", ["-v", name], { shell: true, stdio: "ignore" });
|
|
144
|
+
if (r.status !== 0) {
|
|
145
|
+
log(`${c.red}✗${c.reset} Missing ${c.bold}${name}${c.reset}. Install it and retry.`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function loadConfig(path) {
|
|
151
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ghRepoList(org) {
|
|
155
|
+
const r = spawnSync(
|
|
156
|
+
"gh",
|
|
157
|
+
["repo", "list", org, "--limit", "1000", "--json", "name,isArchived,isFork"],
|
|
158
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
|
159
|
+
);
|
|
160
|
+
if (r.status !== 0) {
|
|
161
|
+
log(`${c.red}✗${c.reset} gh repo list failed:\n${r.stderr || r.stdout}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
const rows = JSON.parse(r.stdout || "[]");
|
|
165
|
+
const names = new Set();
|
|
166
|
+
for (const row of rows) {
|
|
167
|
+
if (row.isArchived || row.isFork) continue;
|
|
168
|
+
names.add(row.name);
|
|
169
|
+
}
|
|
170
|
+
return names;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resolveRepos(cfg, org, teamArg, allRepos) {
|
|
174
|
+
const exclude = new Set(cfg.exclude_from_all || []);
|
|
175
|
+
const visible = ghRepoList(org);
|
|
176
|
+
|
|
177
|
+
if (allRepos) {
|
|
178
|
+
return [...visible].filter((n) => !exclude.has(n)).sort();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let team = (teamArg || "").trim();
|
|
182
|
+
if (!team) team = (cfg.default_team || "").trim();
|
|
183
|
+
|
|
184
|
+
if (!team) {
|
|
185
|
+
return [...visible].sort();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const teams = cfg.teams || {};
|
|
189
|
+
if (!teams[team]) {
|
|
190
|
+
log(
|
|
191
|
+
`${c.red}✗${c.reset} Unknown team ${c.bold}${team}${c.reset}. Valid: ${Object.keys(teams).sort().join(", ")}`
|
|
192
|
+
);
|
|
193
|
+
process.exit(2);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const allowed = teams[team];
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const name of [...allowed].sort()) {
|
|
199
|
+
if (!visible.has(name)) {
|
|
200
|
+
log(`${c.dim}○ skip ${org}/${name} (no access or not in org)${c.reset}`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
out.push(name);
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isSafeTargetDir(abs) {
|
|
209
|
+
const n = normalize(abs);
|
|
210
|
+
const bad = ["/", "/root", "/etc", "/usr", "/bin", "/System", "/Library"];
|
|
211
|
+
return !bad.includes(n);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function cloudLine(org, name, status) {
|
|
215
|
+
const cloud = `${c.blue}☁${c.reset}`;
|
|
216
|
+
if (status === "ok") {
|
|
217
|
+
return ` ${cloud} ${c.green}✓${c.reset} ${c.bold}${org}/${name}${c.reset} ${c.dim}cloned${c.reset}`;
|
|
218
|
+
}
|
|
219
|
+
if (status === "skip") {
|
|
220
|
+
return ` ${cloud} ${c.yellow}○${c.reset} ${org}/${name} ${c.dim}(already there)${c.reset}`;
|
|
221
|
+
}
|
|
222
|
+
if (status === "fail") {
|
|
223
|
+
return ` ${cloud} ${c.red}✗${c.reset} ${org}/${name} ${c.dim}failed${c.reset}`;
|
|
224
|
+
}
|
|
225
|
+
return ` ${cloud} ${c.cyan}…${c.reset} ${org}/${name} ${c.dim}cloning…${c.reset}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function cloneOne(org, name, workspace, useSsh) {
|
|
229
|
+
const gitDir = join(workspace, name, ".git");
|
|
230
|
+
if (existsSync(gitDir)) {
|
|
231
|
+
log(cloudLine(org, name, "skip"));
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
process.stdout.write(`\r${cloudLine(org, name, "pending")}`);
|
|
235
|
+
const args = useSsh
|
|
236
|
+
? ["clone", `git@github.com:${org}/${name}.git`, name]
|
|
237
|
+
: ["repo", "clone", `${org}/${name}`, name];
|
|
238
|
+
const cmd = useSsh ? "git" : "gh";
|
|
239
|
+
const r = spawnSync(cmd, args, {
|
|
240
|
+
cwd: workspace,
|
|
241
|
+
encoding: "utf8",
|
|
242
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
243
|
+
});
|
|
244
|
+
process.stdout.write("\r\x1b[K");
|
|
245
|
+
if (r.status === 0) {
|
|
246
|
+
log(cloudLine(org, name, "ok"));
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
log(cloudLine(org, name, "fail"));
|
|
250
|
+
if (r.stderr) log(`${c.dim}${r.stderr.trim()}${c.reset}`);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function copyCursorPack(workspace, cursorRepoUrl) {
|
|
255
|
+
const rootCursor = join(workspace, ".cursor");
|
|
256
|
+
if (cursorRepoUrl && String(cursorRepoUrl).trim()) {
|
|
257
|
+
const tmp = join(workspace, ".cursor-bootstrap-tmp");
|
|
258
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
259
|
+
log(`${c.dim}→${c.reset} Shallow clone Cursor pack…`);
|
|
260
|
+
const r = spawnSync("git", ["clone", "--depth", "1", String(cursorRepoUrl).trim(), tmp], {
|
|
261
|
+
cwd: workspace,
|
|
262
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
263
|
+
encoding: "utf8",
|
|
264
|
+
});
|
|
265
|
+
if (r.status === 0 && existsSync(join(tmp, ".cursor"))) {
|
|
266
|
+
rmSync(rootCursor, { recursive: true, force: true });
|
|
267
|
+
cpSync(join(tmp, ".cursor"), rootCursor, { recursive: true });
|
|
268
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
269
|
+
log(`${c.green}✓${c.reset} ${c.bold}.cursor${c.reset} ${c.dim}← KOMPLIAN_CURSOR_REPO${c.reset}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
273
|
+
log(`${c.yellow}○${c.reset} KOMPLIAN_CURSOR_REPO clone failed or missing .cursor/ at repo root`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const aiToolsCursor = join(workspace, "ai-tools", ".cursor");
|
|
277
|
+
if (existsSync(aiToolsCursor)) {
|
|
278
|
+
rmSync(rootCursor, { recursive: true, force: true });
|
|
279
|
+
cpSync(aiToolsCursor, rootCursor, { recursive: true });
|
|
280
|
+
log(`${c.green}✓${c.reset} ${c.bold}.cursor${c.reset} ${c.dim}← ai-tools${c.reset}`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
log(
|
|
284
|
+
`${c.dim}○ No workspace .cursor/ (add ai-tools to your team or set KOMPLIAN_CURSOR_REPO).${c.reset}`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function npmInstallEach(workspace) {
|
|
289
|
+
log("");
|
|
290
|
+
log(`${c.cyan}━━ npm install per repo ━━${c.reset}`);
|
|
291
|
+
for (const ent of readdirSync(workspace)) {
|
|
292
|
+
const d = join(workspace, ent);
|
|
293
|
+
if (!statSync(d).isDirectory()) continue;
|
|
294
|
+
const pkg = join(d, "package.json");
|
|
295
|
+
if (!existsSync(pkg)) continue;
|
|
296
|
+
log(`${c.dim}→${c.reset} ${ent}`);
|
|
297
|
+
const r = spawnSync("npm", ["install"], { cwd: d, stdio: "inherit" });
|
|
298
|
+
if (r.status !== 0) {
|
|
299
|
+
log(`${c.yellow}○${c.reset} npm install had issues in ${ent}`);
|
|
300
|
+
} else {
|
|
301
|
+
log(`${c.green}✓${c.reset} ${ent}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function usage() {
|
|
307
|
+
log(`Usage: komplian onboard [options] [dir]`);
|
|
308
|
+
log(` npx komplian onboard --yes clones + npm install (default dir: ~/komplian)`);
|
|
309
|
+
log(``);
|
|
310
|
+
log(` Subcommand onboard implies --install; add --no-install to skip npm install.`);
|
|
311
|
+
log(` -y, --yes Non-interactive (default team from JSON)`);
|
|
312
|
+
log(` -t, --team <slug> Team from komplian-team-repos.json`);
|
|
313
|
+
log(` -i, --install npm install (on by default with onboard; optional without subcommand)`);
|
|
314
|
+
log(` --no-install Skip npm install (only after onboard)`);
|
|
315
|
+
log(` --all-repos Clone all visible org repos (minus exclude list)`);
|
|
316
|
+
log(` --ssh Use SSH URLs (git@github.com:...)`);
|
|
317
|
+
log(` --list-teams Print team slugs and exit`);
|
|
318
|
+
log(` -h, --help This help`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function parseArgs(argv) {
|
|
322
|
+
const out = {
|
|
323
|
+
yes: false,
|
|
324
|
+
install: false,
|
|
325
|
+
noInstall: false,
|
|
326
|
+
allRepos: false,
|
|
327
|
+
ssh: false,
|
|
328
|
+
listTeams: false,
|
|
329
|
+
help: false,
|
|
330
|
+
team: "",
|
|
331
|
+
workspace: "",
|
|
332
|
+
};
|
|
333
|
+
const rest = [];
|
|
334
|
+
for (let i = 0; i < argv.length; i++) {
|
|
335
|
+
const a = argv[i];
|
|
336
|
+
if (a === "--yes" || a === "-y") out.yes = true;
|
|
337
|
+
else if (a === "--no-install") out.noInstall = true;
|
|
338
|
+
else if (a === "-i" || a === "--install") out.install = true;
|
|
339
|
+
else if (a === "--all-repos") out.allRepos = true;
|
|
340
|
+
else if (a === "--ssh") out.ssh = true;
|
|
341
|
+
else if (a === "--list-teams") out.listTeams = true;
|
|
342
|
+
else if (a === "-h" || a === "--help") out.help = true;
|
|
343
|
+
else if (a === "-t" || a === "--team") {
|
|
344
|
+
out.team = argv[++i] || "";
|
|
345
|
+
} else if (a.startsWith("-")) {
|
|
346
|
+
log(`${c.red}✗${c.reset} Unknown option: ${a}`);
|
|
347
|
+
usage();
|
|
348
|
+
process.exit(1);
|
|
349
|
+
} else rest.push(a);
|
|
350
|
+
}
|
|
351
|
+
if (rest[0]) out.workspace = rest[0];
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function pickTeam(cfg) {
|
|
356
|
+
const teams = Object.keys(cfg.teams || {}).sort();
|
|
357
|
+
const def = (cfg.default_team || "").trim();
|
|
358
|
+
log(`${c.cyan}Teams:${c.reset}`);
|
|
359
|
+
teams.forEach((t, i) => {
|
|
360
|
+
const mark = t === def ? ` ${c.dim}(default)${c.reset}` : "";
|
|
361
|
+
log(` ${i + 1}. ${t}${mark}`);
|
|
362
|
+
});
|
|
363
|
+
const rl = createInterface({ input, output });
|
|
364
|
+
const ans = await rl.question(
|
|
365
|
+
`\n${c.bold}Choose number or name${c.reset} [${def}]: `
|
|
366
|
+
);
|
|
367
|
+
rl.close();
|
|
368
|
+
const trimmed = (ans || "").trim();
|
|
369
|
+
if (!trimmed) return def;
|
|
370
|
+
const n = parseInt(trimmed, 10);
|
|
371
|
+
if (!Number.isNaN(n) && n >= 1 && n <= teams.length) return teams[n - 1];
|
|
372
|
+
if (teams.includes(trimmed)) return trimmed;
|
|
373
|
+
return def || teams[0] || "";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function normalizeArgv(argv) {
|
|
377
|
+
const a = [...argv];
|
|
378
|
+
let fromOnboardSubcommand = false;
|
|
379
|
+
if (a[0] === "onboard") {
|
|
380
|
+
a.shift();
|
|
381
|
+
fromOnboardSubcommand = true;
|
|
382
|
+
}
|
|
383
|
+
return { argv: a, fromOnboardSubcommand };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function main() {
|
|
387
|
+
const configPath = join(__dirname, "komplian-team-repos.json");
|
|
388
|
+
const { argv, fromOnboardSubcommand } = normalizeArgv(process.argv.slice(2));
|
|
389
|
+
const args = parseArgs(argv);
|
|
390
|
+
if (fromOnboardSubcommand && !args.noInstall && !args.listTeams && !args.help) {
|
|
391
|
+
args.install = true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (args.help) {
|
|
395
|
+
usage();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (args.listTeams) {
|
|
400
|
+
if (!existsSync(configPath)) {
|
|
401
|
+
log(`${c.red}✗${c.reset} Missing ${configPath}`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
needCmd("gh");
|
|
405
|
+
if (!ghOk()) {
|
|
406
|
+
log(`${c.red}✗${c.reset} Not logged in to GitHub. Run: ${c.bold}gh auth login -h github.com -s repo -s read:org -w${c.reset}`);
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
const cfg = loadConfig(configPath);
|
|
410
|
+
const orgList = process.env.KOMPLIAN_ORG || cfg.org || "Komplian";
|
|
411
|
+
verifyOrgMembership(orgList);
|
|
412
|
+
log(Object.keys(cfg.teams || {}).sort().join("\n"));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!existsSync(configPath)) {
|
|
417
|
+
log(`${c.red}✗${c.reset} Missing config: ${configPath}`);
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
needCmd("git");
|
|
422
|
+
needCmd("gh");
|
|
423
|
+
|
|
424
|
+
const nodeMajor = Number(process.versions.node.split(".")[0], 10);
|
|
425
|
+
if (nodeMajor < 18) {
|
|
426
|
+
log(`${c.red}✗${c.reset} Need Node 18+. You have ${process.versions.node}.`);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!ghOk()) {
|
|
431
|
+
if (!runGhAuth()) {
|
|
432
|
+
log(`${c.red}✗${c.reset} GitHub authentication did not complete.`);
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
log(`${c.green}✓${c.reset} GitHub CLI session OK`);
|
|
437
|
+
|
|
438
|
+
const cfg = loadConfig(configPath);
|
|
439
|
+
const org = process.env.KOMPLIAN_ORG || cfg.org || "Komplian";
|
|
440
|
+
|
|
441
|
+
verifyOrgMembership(org);
|
|
442
|
+
logGhIdentity();
|
|
443
|
+
log(
|
|
444
|
+
`${c.green}✓${c.reset} Org ${c.bold}${org}${c.reset}: ${c.dim}active membership verified (GitHub API)${c.reset}`
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
banner();
|
|
448
|
+
|
|
449
|
+
let team = args.team;
|
|
450
|
+
if (!team && !args.yes && !args.allRepos) {
|
|
451
|
+
team = await pickTeam(cfg);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const repos = resolveRepos(cfg, org, team, args.allRepos);
|
|
455
|
+
if (repos.length === 0) {
|
|
456
|
+
log(`${c.red}✗${c.reset} No repositories to clone (permissions / team / org).`);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let workspace = args.workspace.trim();
|
|
461
|
+
if (!workspace) {
|
|
462
|
+
workspace = join(homedir(), "komplian");
|
|
463
|
+
}
|
|
464
|
+
const abs = resolve(workspace.replace(/^~(?=$|[/\\])/, homedir()));
|
|
465
|
+
if (!isSafeTargetDir(abs)) {
|
|
466
|
+
log(`${c.red}✗${c.reset} Refusing unsafe target directory: ${abs}`);
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
mkdirSync(abs, { recursive: true });
|
|
471
|
+
log("");
|
|
472
|
+
log(`${c.cyan}━━ Workspace ━━${c.reset} ${c.bold}${abs}${c.reset}`);
|
|
473
|
+
log(`${c.cyan}━━ Org ━━${c.reset} ${c.bold}${org}${c.reset}`);
|
|
474
|
+
if (team) log(`${c.cyan}━━ Team ━━${c.reset} ${c.bold}${team}${c.reset}`);
|
|
475
|
+
log(`${c.cyan}━━ Repos (${repos.length}) ━━${c.reset}`);
|
|
476
|
+
log("");
|
|
477
|
+
|
|
478
|
+
let failed = 0;
|
|
479
|
+
for (const name of repos) {
|
|
480
|
+
const ok = cloneOne(org, name, abs, args.ssh);
|
|
481
|
+
if (!ok) failed += 1;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
copyCursorPack(abs, process.env.KOMPLIAN_CURSOR_REPO);
|
|
485
|
+
|
|
486
|
+
if (args.install) {
|
|
487
|
+
needCmd("npm");
|
|
488
|
+
npmInstallEach(abs);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
log("");
|
|
492
|
+
log(`${c.cyan}━━ Done ━━${c.reset}`);
|
|
493
|
+
if (failed > 0) {
|
|
494
|
+
log(`${c.yellow}○${c.reset} ${failed} repo(s) failed — check access and retry.`);
|
|
495
|
+
}
|
|
496
|
+
log(`${c.green}✓${c.reset} Open in Cursor: ${c.bold}File → Open Folder → ${abs}${c.reset}`);
|
|
497
|
+
log(`${c.dim} Copy .env.example → .env per project; secrets via 1Password — never commit.${c.reset}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
main().catch((e) => {
|
|
501
|
+
console.error(e);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Map team slug (-t) → repo names in org. Align slugs with GitHub Team names or internal labels. Adjust when teams/repos change.",
|
|
3
|
+
"org": "Komplian",
|
|
4
|
+
"default_team": "platform",
|
|
5
|
+
"exclude_from_all": [],
|
|
6
|
+
"teams": {
|
|
7
|
+
"platform": ["app", "api", "web", "admin", "docs", "ai-tools"],
|
|
8
|
+
"backend": ["api", "ai-tools"],
|
|
9
|
+
"frontend": ["app", "web", "docs", "ai-tools"],
|
|
10
|
+
"admin": ["admin", "api", "ai-tools"],
|
|
11
|
+
"docs": ["docs", "web", "ai-tools"],
|
|
12
|
+
"growth": ["web", "docs", "ai-tools"]
|
|
13
|
+
}
|
|
14
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "komplian",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Komplian developer workspace setup: GitHub CLI auth + clone repos by team (zero manual repo clone for developers — use npx).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"komplian": "komplian-onboard.mjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"komplian-onboard.mjs",
|
|
14
|
+
"komplian-team-repos.json",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"prepublishOnly": "node --check komplian-onboard.mjs"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"komplian",
|
|
25
|
+
"onboarding",
|
|
26
|
+
"github",
|
|
27
|
+
"cli"
|
|
28
|
+
],
|
|
29
|
+
"license": "UNLICENSED",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/Komplian/komplian.git"
|
|
33
|
+
}
|
|
34
|
+
}
|