@next-generation-collective/factory 0.3.0 → 0.3.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/lib/banner.js ADDED
@@ -0,0 +1,86 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { basename, join } from "node:path";
5
+ import { colors } from "./util.js";
6
+
7
+ // The Next-Gen mark in the terminal: an N whose diagonal runs into a G, in the brand gold.
8
+ const MARK = [
9
+ "█▄ █ ▄▀▀▀▀",
10
+ "█ ▀▄█ █ ▄▄",
11
+ "█ █ ▀▄▄▄▀",
12
+ ];
13
+
14
+ const truecolor = /truecolor|24bit/i.test(process.env.COLORTERM ?? "");
15
+ const gold = (t) => colors.paint(truecolor ? "38;2;245;166;35" : "38;5;214", t);
16
+ const cream = (t) => colors.paint(truecolor ? "38;2;253;251;247" : "97", t);
17
+
18
+ const width = (text) => [...text.replace(/\u001b\[[0-9;]*m/g, "")].length;
19
+
20
+ // The builder's first name, from their git identity or, on a Mac, their account's full
21
+ // name. Nothing is sent anywhere.
22
+ export function firstName() {
23
+ const ask = (cmd, args) => {
24
+ try {
25
+ return execFileSync(cmd, args, { encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"] }).trim();
26
+ } catch {
27
+ return "";
28
+ }
29
+ };
30
+ const full = ask("git", ["config", "user.name"]) || (process.platform === "darwin" ? ask("id", ["-F"]) : "");
31
+ return full.split(/\s+/)[0] || null;
32
+ }
33
+
34
+ // One small file in the builder's own settings folder, so the first welcome happens once.
35
+ function welcomeFile() {
36
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
37
+ return join(base, "next-gen", "welcomed");
38
+ }
39
+
40
+ export function isFirstVisit() {
41
+ try {
42
+ return !existsSync(welcomeFile());
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ export function rememberVisit() {
49
+ try {
50
+ const file = welcomeFile();
51
+ mkdirSync(join(file, ".."), { recursive: true });
52
+ writeFileSync(file, `${new Date().toISOString().slice(0, 10)}\n`);
53
+ } catch {
54
+ // A read-only home folder only means the long welcome shows again.
55
+ }
56
+ }
57
+
58
+ export function welcomeLines({ name, first, version }) {
59
+ const you = name ? `, ${name}` : "";
60
+ return first
61
+ ? [
62
+ colors.bold(cream(`Welcome to Next-Gen Factory${you}.`)),
63
+ cream("You are one of the builders of the AI economy."),
64
+ `${gold("Africa builds.")} ${colors.dim(`v${version}`)}`,
65
+ ]
66
+ : [
67
+ colors.bold(cream(`Welcome back${you}.`)),
68
+ cream("Every build you prove makes every builder stronger."),
69
+ `${gold("Africa builds.")} ${colors.dim(`v${version}`)}`,
70
+ ];
71
+ }
72
+
73
+ export function banner(version, root, { name = firstName(), first = isFirstVisit() } = {}) {
74
+ const lines = welcomeLines({ name, first, version });
75
+ const rows = lines.map((line, i) => ` ${gold(MARK[i] ?? " ".repeat(width(MARK[0])))} ${line}`);
76
+ const inner = Math.max(...rows.map(width)) + 2;
77
+ const edge = (l, r) => colors.dim(`${l}${"─".repeat(inner)}${r}`);
78
+ if (first) rememberVisit();
79
+ return [
80
+ edge("╭", "╮"),
81
+ ...rows.map((row) => `${colors.dim("│")}${row}${" ".repeat(inner - width(row))}${colors.dim("│")}`),
82
+ edge("╰", "╯"),
83
+ colors.dim(` Reading ${basename(root) || root}...`),
84
+ "",
85
+ ].join("\n");
86
+ }
package/lib/cli.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { homedir } from "node:os";
2
+ import { banner } from "./banner.js";
3
+ import { parse as parse_, resolve } from "node:path";
1
4
  import { createRequire } from "node:module";
2
5
  import {
3
6
  approveCommand,
@@ -95,6 +98,20 @@ Nothing is written into your project until you start a job. Nothing is called
95
98
  done until the checks pass, and it never says "bug-free" or "compliant".`);
96
99
  }
97
100
 
101
+ // Your home folder or the top of the disk is never one project. Reading it means reading
102
+ // every file on the computer, which takes minutes and shows nothing.
103
+ export function notAProjectFolder(root, home = homedir()) {
104
+ const here = resolve(root);
105
+ if (here !== resolve(home) && here !== parse_(here).root) return null;
106
+ return [
107
+ `This is ${here === resolve(home) ? "your home folder" : "the top of your disk"}, not a project.`,
108
+ "Go into a project folder first, then run it again. For example:",
109
+ "",
110
+ " cd my-project",
111
+ " npx @next-generation-collective/factory",
112
+ ].join("\n");
113
+ }
114
+
98
115
  export async function main(argv) {
99
116
  const { flags, positional } = parse(argv);
100
117
  const root = process.cwd();
@@ -110,6 +127,15 @@ export async function main(argv) {
110
127
  return 0;
111
128
  }
112
129
 
130
+ if (!command || (!COMMANDS.has(command) && (rest.length > 0 || /\s/.test(command)))) {
131
+ const refusal = notAProjectFolder(root);
132
+ if (refusal) {
133
+ console.error(refusal);
134
+ return 1;
135
+ }
136
+ if (process.stdout.isTTY) console.log(banner(VERSION, root));
137
+ }
138
+
113
139
  if (!command) return start(root, flags);
114
140
 
115
141
  if (!COMMANDS.has(command)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next-generation-collective/factory",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Checks the work you build with AI, proves it works, and learns from every builder. Built in the open, in Africa.",
5
5
  "type": "module",
6
6
  "bin": {