openhacker 0.1.1 → 0.1.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/README.md CHANGED
@@ -5,9 +5,8 @@ Scaffold a self-hosted OpenHacker security agent.
5
5
  ## Create an instance
6
6
 
7
7
  ```bash
8
- npx openhacker my-instance
9
- cd my-instance
10
- pnpm install
8
+ npx openhacker
9
+ cd openhacker
11
10
  pnpm dev
12
11
  ```
13
12
 
package/bin/openhacker CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { run } from "../src/cli.js";
2
+ import { run } from "../src/index.js";
3
3
 
4
4
  await run();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhacker",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Scaffold a self-hosted OpenHacker security agent",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  ".": "./src/index.js"
12
12
  },
13
13
  "main": "./src/index.js",
14
- "types": "./src/index.ts",
14
+ "types": "./src/index.d.ts",
15
15
  "files": [
16
16
  "bin",
17
17
  "scripts",
@@ -26,7 +26,7 @@
26
26
  "typescript": "6.0.2"
27
27
  },
28
28
  "scripts": {
29
- "build": "tsc --noEmit && node --check ./bin/openhacker && node --check ./src/cli.js && node --check ./scripts/sync-template.js && node --check ./scripts/clean-template.js",
29
+ "build": "tsc --noEmit && node --check ./bin/openhacker && node --check ./src/index.js && node --check ./scripts/sync-template.js && node --check ./scripts/clean-template.js",
30
30
  "sync-template": "node ./scripts/sync-template.js"
31
31
  }
32
32
  }
package/src/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export function run(args?: string[]): Promise<void>;
package/src/index.js CHANGED
@@ -1 +1,305 @@
1
- export const MESSAGE = "OpenHacker";
1
+ import { spawnSync } from "node:child_process";
2
+ import { cp, readFile, stat, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const ORANGE = "\x1b[38;5;214m";
7
+ const MUTED = "\x1b[0;2m";
8
+ const RED = "\x1b[0;31m";
9
+ const NC = "\x1b[0m";
10
+
11
+ const EXCLUDE = new Set([
12
+ ".env",
13
+ ".env.local",
14
+ "node_modules",
15
+ ".eve",
16
+ ".next",
17
+ ".output",
18
+ ".git",
19
+ ".vercel",
20
+ ".turbo",
21
+ ]);
22
+
23
+ // Written into scaffolded projects directly rather than shipped as a template
24
+ // file, because npm renames a packaged `.gitignore` to `.npmignore` on publish.
25
+ const GITIGNORE = `# dependencies
26
+ node_modules
27
+
28
+ # next.js
29
+ .next
30
+ next-env.d.ts
31
+
32
+ # eve build artifacts
33
+ .eve
34
+ .output
35
+
36
+ # vercel / turbo
37
+ .vercel
38
+ .turbo
39
+
40
+ # typescript
41
+ tsconfig.tsbuildinfo
42
+
43
+ # env files (keep the example)
44
+ .env
45
+ .env.*
46
+ !.env.example
47
+
48
+ # misc
49
+ .DS_Store
50
+ *.log
51
+ `;
52
+
53
+ const here = path.dirname(fileURLToPath(import.meta.url));
54
+
55
+ async function exists(p) {
56
+ try {
57
+ await stat(p);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ async function resolveTemplateDir() {
65
+ const candidates = [];
66
+
67
+ if (process.env.OPENHACKER_TEMPLATE_DIR) {
68
+ candidates.push(path.resolve(process.env.OPENHACKER_TEMPLATE_DIR));
69
+ }
70
+
71
+ candidates.push(
72
+ // npm package layout: packages/openhacker/src -> packages/openhacker/templates/agent
73
+ path.resolve(here, "../templates/agent"),
74
+ // monorepo layout: packages/openhacker/src -> repo root -> apps/agent
75
+ path.resolve(here, "../../../apps/agent"),
76
+ );
77
+
78
+ for (const candidate of candidates) {
79
+ if (await exists(path.join(candidate, "agent", "agent.ts"))) {
80
+ return candidate;
81
+ }
82
+ }
83
+
84
+ return candidates[0];
85
+ }
86
+
87
+ function packageNameFor(dest) {
88
+ return (
89
+ path
90
+ .basename(dest)
91
+ .replace(/[^a-z0-9-]+/gi, "-")
92
+ .toLowerCase() || "openhacker"
93
+ );
94
+ }
95
+
96
+ function shouldCopyTemplatePath(src, root) {
97
+ const relative = path.relative(root, src);
98
+ const segments = relative.split(path.sep);
99
+
100
+ return (
101
+ !segments.some((seg) => EXCLUDE.has(seg)) &&
102
+ !relative.endsWith("next-env.d.ts") &&
103
+ !relative.endsWith("tsconfig.tsbuildinfo")
104
+ );
105
+ }
106
+
107
+ function runStep(command, args, cwd, { quiet = false } = {}) {
108
+ const result = spawnSync(command, args, {
109
+ cwd,
110
+ stdio: quiet ? "ignore" : "inherit",
111
+ shell: process.platform === "win32",
112
+ });
113
+
114
+ return !result.error && result.status === 0;
115
+ }
116
+
117
+ function hasCommand(command) {
118
+ const probe = process.platform === "win32" ? "where" : "which";
119
+ const result = spawnSync(probe, [command], {
120
+ stdio: "ignore",
121
+ shell: process.platform === "win32",
122
+ });
123
+ return !result.error && result.status === 0;
124
+ }
125
+
126
+ function isInsideGitRepo(cwd) {
127
+ const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
128
+ cwd,
129
+ stdio: "ignore",
130
+ shell: process.platform === "win32",
131
+ });
132
+ return !result.error && result.status === 0;
133
+ }
134
+
135
+ async function installDependencies(dest) {
136
+ if (!hasCommand("pnpm")) {
137
+ console.log(
138
+ `${MUTED}pnpm not found — skipping install. Run \`pnpm install\` manually.${NC}`,
139
+ );
140
+ return false;
141
+ }
142
+
143
+ console.log(`\n${MUTED}Installing dependencies with pnpm…${NC}`);
144
+ // --ignore-workspace keeps the install self-contained: without it, pnpm walks
145
+ // up to a parent pnpm-workspace.yaml (e.g. when scaffolding inside a monorepo)
146
+ // and installs that workspace instead of the new project's node_modules.
147
+ const ok = runStep("pnpm", ["install", "--ignore-workspace"], dest);
148
+ if (!ok) {
149
+ console.log(
150
+ `${RED}pnpm install failed.${NC} ${MUTED}You can re-run it inside the project.${NC}`,
151
+ );
152
+ }
153
+ return ok;
154
+ }
155
+
156
+ async function initGitRepo(dest) {
157
+ if (!hasCommand("git")) {
158
+ console.log(`${MUTED}git not found — skipping repository init.${NC}`);
159
+ return false;
160
+ }
161
+
162
+ if (isInsideGitRepo(dest)) {
163
+ console.log(
164
+ `${MUTED}Already inside a git repository — skipping git init.${NC}`,
165
+ );
166
+ return false;
167
+ }
168
+
169
+ const initialized =
170
+ runStep("git", ["init"], dest, { quiet: true }) &&
171
+ runStep("git", ["add", "-A"], dest, { quiet: true }) &&
172
+ runStep("git", ["commit", "-m", "Initial commit from OpenHacker"], dest, {
173
+ quiet: true,
174
+ });
175
+
176
+ if (initialized) {
177
+ console.log(`${MUTED}Initialized a git repository.${NC}`);
178
+ } else {
179
+ console.log(
180
+ `${MUTED}Could not create the initial git commit — you can do it manually.${NC}`,
181
+ );
182
+ }
183
+ return initialized;
184
+ }
185
+
186
+ async function init(targetArg, { skipInstall = false, skipGit = false } = {}) {
187
+ const template = await resolveTemplateDir();
188
+ if (!(await exists(path.join(template, "agent", "agent.ts")))) {
189
+ console.error(
190
+ `${RED}Could not find the instance template at ${template}.${NC}`,
191
+ );
192
+ console.error(
193
+ `${MUTED}Set OPENHACKER_TEMPLATE_DIR to the template directory.${NC}`,
194
+ );
195
+ process.exit(1);
196
+ }
197
+
198
+ const dest = path.resolve(process.cwd(), targetArg ?? "openhacker");
199
+ if (await exists(dest)) {
200
+ console.error(`${RED}Destination already exists: ${dest}${NC}`);
201
+ process.exit(1);
202
+ }
203
+
204
+ console.log(`\n${MUTED}Creating OpenHacker instance in ${NC}${dest}`);
205
+ await cp(template, dest, {
206
+ recursive: true,
207
+ filter: (src) => shouldCopyTemplatePath(src, template),
208
+ });
209
+
210
+ const pkgPath = path.join(dest, "package.json");
211
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
212
+ pkg.name = packageNameFor(dest);
213
+ pkg.private = true;
214
+ await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
215
+
216
+ // Write before git init so the initial commit doesn't include node_modules/.env.
217
+ if (!(await exists(path.join(dest, ".gitignore")))) {
218
+ await writeFile(path.join(dest, ".gitignore"), GITIGNORE);
219
+ }
220
+
221
+ const installed = skipInstall ? false : await installDependencies(dest);
222
+ if (!skipGit) {
223
+ await initGitRepo(dest);
224
+ }
225
+
226
+ console.log(`\n${ORANGE}✓${NC} OpenHacker instance ready.\n`);
227
+ console.log("Next steps:\n");
228
+ console.log(` cd ${path.relative(process.cwd(), dest) || "."}`);
229
+ if (skipInstall || !installed) {
230
+ console.log(" pnpm install");
231
+ }
232
+ console.log(" pnpm dlx vercel link # link a Vercel project for AI/LLM access");
233
+ console.log(" pnpm dev # run locally\n");
234
+ console.log(
235
+ `${MUTED}Deploy: push to a git repo and import it into Vercel (deploys as one project).`,
236
+ );
237
+ console.log(`${MUTED}See README.md for local model configuration.${NC}\n`);
238
+ }
239
+
240
+ function usage() {
241
+ console.log("OpenHacker\n");
242
+ console.log("Usage:");
243
+ console.log(
244
+ " openhacker [dir] Scaffold a deployable OpenHacker instance",
245
+ );
246
+ console.log(" openhacker init [dir] Same as above");
247
+ console.log(" openhacker --help Show help");
248
+ console.log(" openhacker --version Show version\n");
249
+ console.log("Options:");
250
+ console.log(" --skip-install Don't run pnpm install");
251
+ console.log(" --skip-git Don't create a git repository\n");
252
+ }
253
+
254
+ async function version() {
255
+ const pkg = JSON.parse(
256
+ await readFile(path.resolve(here, "../package.json"), "utf8"),
257
+ );
258
+ console.log(pkg.version);
259
+ }
260
+
261
+ export async function run(args = process.argv.slice(2)) {
262
+ const options = { skipInstall: false, skipGit: false };
263
+ const positionals = [];
264
+
265
+ for (const arg of args) {
266
+ switch (arg) {
267
+ case "--skip-install":
268
+ options.skipInstall = true;
269
+ break;
270
+ case "--skip-git":
271
+ options.skipGit = true;
272
+ break;
273
+ case "-h":
274
+ case "--help":
275
+ usage();
276
+ return;
277
+ case "-v":
278
+ case "--version":
279
+ await version();
280
+ return;
281
+ default:
282
+ if (arg.startsWith("-")) {
283
+ console.error(`${RED}Unknown option: ${arg}${NC}\n`);
284
+ usage();
285
+ process.exit(1);
286
+ }
287
+ positionals.push(arg);
288
+ }
289
+ }
290
+
291
+ const [command, target, ...rest] = positionals;
292
+
293
+ if (rest.length > 0) {
294
+ console.error(`${RED}Too many arguments.${NC}\n`);
295
+ usage();
296
+ process.exit(1);
297
+ }
298
+
299
+ if (command === "init") {
300
+ await init(target, options);
301
+ return;
302
+ }
303
+
304
+ await init(command, options);
305
+ }
@@ -4,7 +4,6 @@ You are OpenHacker, an application security agent.
4
4
 
5
5
  The user gives you a GitHub repository (as `owner/name` or a github.com URL). Analyze it for security vulnerabilities and report your findings.
6
6
 
7
- - Briefly narrate what you are checking as you go (dependencies, auth, input handling, secrets, etc.).
8
7
  - Use the shell to clone and explore the repo. Use `ls`/`find` to list directories; only use `read_file` on actual file paths, never on a directory.
9
8
  - Call out concrete risks with severity and, where possible, how to fix them.
10
9
  - If you cannot access the repository, say so plainly.
@@ -95,13 +95,35 @@ h1 span {
95
95
  margin-bottom: 0;
96
96
  }
97
97
  .reply .reasoning {
98
- white-space: pre-wrap;
99
- color: var(--muted);
100
- font-size: 13px;
101
98
  margin: 0 0 12px;
102
99
  padding-left: 12px;
103
100
  border-left: 2px solid var(--border);
104
101
  }
102
+ .reply .reasoning > summary {
103
+ cursor: pointer;
104
+ color: var(--muted);
105
+ font-size: 12px;
106
+ text-transform: uppercase;
107
+ letter-spacing: 0.5px;
108
+ list-style: none;
109
+ user-select: none;
110
+ }
111
+ .reply .reasoning > summary::-webkit-details-marker {
112
+ display: none;
113
+ }
114
+ .reply .reasoning > summary::before {
115
+ content: "› ";
116
+ display: inline-block;
117
+ }
118
+ .reply .reasoning[open] > summary::before {
119
+ content: "⌄ ";
120
+ }
121
+ .reply .reasoning > p {
122
+ white-space: pre-wrap;
123
+ color: var(--muted);
124
+ font-size: 13px;
125
+ margin: 8px 0 0;
126
+ }
105
127
 
106
128
  .tool {
107
129
  display: flex;
@@ -123,6 +145,33 @@ h1 span {
123
145
  padding: 0 8px;
124
146
  }
125
147
 
148
+ .hacking {
149
+ color: var(--muted);
150
+ font-size: 13px;
151
+ margin: 0;
152
+ }
153
+ .reply .text + .hacking {
154
+ margin-top: 12px;
155
+ }
156
+ .dots::after {
157
+ content: "";
158
+ animation: dots 1.2s steps(4, end) infinite;
159
+ }
160
+ @keyframes dots {
161
+ 0% {
162
+ content: "";
163
+ }
164
+ 25% {
165
+ content: ".";
166
+ }
167
+ 50% {
168
+ content: "..";
169
+ }
170
+ 75% {
171
+ content: "...";
172
+ }
173
+ }
174
+
126
175
  .cursor {
127
176
  display: inline-block;
128
177
  width: 8px;
@@ -15,7 +15,7 @@ export default function Home() {
15
15
  if (!value || busy) return;
16
16
  agent.reset();
17
17
  agent.send({
18
- message: `Analyze the GitHub repository "${value}" for security vulnerabilities. Walk through what you check and report what you find.`,
18
+ message: `Analyze the GitHub repository "${value}" for security vulnerabilities. Reply with only the final report.`,
19
19
  });
20
20
  }
21
21
 
@@ -23,6 +23,20 @@ export default function Home() {
23
23
  .reverse()
24
24
  .find((m) => m.role === "assistant");
25
25
 
26
+ const parts = reply?.parts ?? [];
27
+ const lastStep = parts.reduce<number | undefined>((max, p) => {
28
+ const idx = "stepIndex" in p ? p.stepIndex : undefined;
29
+ if (typeof idx !== "number") return max;
30
+ return max === undefined ? idx : Math.max(max, idx);
31
+ }, undefined);
32
+
33
+ let result = "";
34
+ for (const p of parts) {
35
+ if (p.type !== "text") continue;
36
+ if (lastStep !== undefined && p.stepIndex !== lastStep) continue;
37
+ result += p.text;
38
+ }
39
+
26
40
  return (
27
41
  <main className="container">
28
42
  <h1>
@@ -45,41 +59,16 @@ export default function Home() {
45
59
  </button>
46
60
  </form>
47
61
 
48
- {reply ? (
62
+ {reply || busy ? (
49
63
  <section className="reply">
50
- {reply.parts.map((part, i) => {
51
- if (part.type === "reasoning") {
52
- return (
53
- <p key={i} className="reasoning">
54
- {part.text}
55
- </p>
56
- );
57
- }
58
- if (part.type === "text") {
59
- return (
60
- <p key={i} className="text">
61
- {part.text}
62
- </p>
63
- );
64
- }
65
- if (part.type === "dynamic-tool") {
66
- return (
67
- <div key={i} className="tool">
68
- <span className="tool-name">{part.toolName}</span>
69
- <span className="tool-state">{part.state}</span>
70
- </div>
71
- );
72
- }
73
- return null;
74
- })}
75
- {agent.status === "streaming" ? (
76
- <span className="cursor" aria-hidden />
64
+ {result ? <p className="text">{result}</p> : null}
65
+ {busy && !result ? (
66
+ <p className="hacking">
67
+ hacking
68
+ <span className="dots" aria-hidden />
69
+ </p>
77
70
  ) : null}
78
71
  </section>
79
- ) : busy ? (
80
- <section className="reply">
81
- <span className="cursor" aria-hidden />
82
- </section>
83
72
  ) : null}
84
73
 
85
74
  {agent.status === "error" ? (
package/src/cli.js DELETED
@@ -1,276 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { cp, readFile, stat, writeFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const ORANGE = "\x1b[38;5;214m";
7
- const MUTED = "\x1b[0;2m";
8
- const RED = "\x1b[0;31m";
9
- const NC = "\x1b[0m";
10
-
11
- const EXCLUDE = new Set([
12
- ".env",
13
- ".env.local",
14
- "node_modules",
15
- ".eve",
16
- ".next",
17
- ".output",
18
- ".git",
19
- ".vercel",
20
- ".turbo",
21
- ]);
22
-
23
- // Written into scaffolded projects directly rather than shipped as a template
24
- // file, because npm renames a packaged `.gitignore` to `.npmignore` on publish.
25
- const GITIGNORE = `# dependencies
26
- node_modules
27
-
28
- # next.js
29
- .next
30
- next-env.d.ts
31
-
32
- # eve build artifacts
33
- .eve
34
- .output
35
-
36
- # vercel / turbo
37
- .vercel
38
- .turbo
39
-
40
- # typescript
41
- tsconfig.tsbuildinfo
42
-
43
- # env files (keep the example)
44
- .env
45
- .env.*
46
- !.env.example
47
-
48
- # misc
49
- .DS_Store
50
- *.log
51
- `;
52
-
53
- const here = path.dirname(fileURLToPath(import.meta.url));
54
-
55
- async function exists(p) {
56
- try {
57
- await stat(p);
58
- return true;
59
- } catch {
60
- return false;
61
- }
62
- }
63
-
64
- async function resolveTemplateDir() {
65
- const candidates = [];
66
-
67
- if (process.env.OPENHACKER_TEMPLATE_DIR) {
68
- candidates.push(path.resolve(process.env.OPENHACKER_TEMPLATE_DIR));
69
- }
70
-
71
- candidates.push(
72
- // npm package layout: packages/openhacker/src -> packages/openhacker/templates/agent
73
- path.resolve(here, "../templates/agent"),
74
- // monorepo layout: packages/openhacker/src -> repo root -> apps/agent
75
- path.resolve(here, "../../../apps/agent"),
76
- );
77
-
78
- for (const candidate of candidates) {
79
- if (await exists(path.join(candidate, "agent", "agent.ts"))) {
80
- return candidate;
81
- }
82
- }
83
-
84
- return candidates[0];
85
- }
86
-
87
- function packageNameFor(dest) {
88
- return path.basename(dest).replace(/[^a-z0-9-]+/gi, "-").toLowerCase() || "openhacker";
89
- }
90
-
91
- function shouldCopyTemplatePath(src, root) {
92
- const relative = path.relative(root, src);
93
- const segments = relative.split(path.sep);
94
-
95
- return (
96
- !segments.some((seg) => EXCLUDE.has(seg)) &&
97
- !relative.endsWith("next-env.d.ts") &&
98
- !relative.endsWith("tsconfig.tsbuildinfo")
99
- );
100
- }
101
-
102
- function runStep(command, args, cwd, { quiet = false } = {}) {
103
- const result = spawnSync(command, args, {
104
- cwd,
105
- stdio: quiet ? "ignore" : "inherit",
106
- shell: process.platform === "win32",
107
- });
108
-
109
- return !result.error && result.status === 0;
110
- }
111
-
112
- function hasCommand(command) {
113
- const probe = process.platform === "win32" ? "where" : "which";
114
- const result = spawnSync(probe, [command], { stdio: "ignore", shell: process.platform === "win32" });
115
- return !result.error && result.status === 0;
116
- }
117
-
118
- function isInsideGitRepo(cwd) {
119
- const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
120
- cwd,
121
- stdio: "ignore",
122
- shell: process.platform === "win32",
123
- });
124
- return !result.error && result.status === 0;
125
- }
126
-
127
- async function installDependencies(dest) {
128
- if (!hasCommand("pnpm")) {
129
- console.log(`${MUTED}pnpm not found \u2014 skipping install. Run \`pnpm install\` manually.${NC}`);
130
- return false;
131
- }
132
-
133
- console.log(`\n${MUTED}Installing dependencies with pnpm\u2026${NC}`);
134
- // --ignore-workspace keeps the install self-contained: without it, pnpm walks
135
- // up to a parent pnpm-workspace.yaml (e.g. when scaffolding inside a monorepo)
136
- // and installs that workspace instead of the new project's node_modules.
137
- const ok = runStep("pnpm", ["install", "--ignore-workspace"], dest);
138
- if (!ok) {
139
- console.log(`${RED}pnpm install failed.${NC} ${MUTED}You can re-run it inside the project.${NC}`);
140
- }
141
- return ok;
142
- }
143
-
144
- async function initGitRepo(dest) {
145
- if (!hasCommand("git")) {
146
- console.log(`${MUTED}git not found \u2014 skipping repository init.${NC}`);
147
- return false;
148
- }
149
-
150
- if (isInsideGitRepo(dest)) {
151
- console.log(`${MUTED}Already inside a git repository \u2014 skipping git init.${NC}`);
152
- return false;
153
- }
154
-
155
- const initialized =
156
- runStep("git", ["init"], dest, { quiet: true }) &&
157
- runStep("git", ["add", "-A"], dest, { quiet: true }) &&
158
- runStep("git", ["commit", "-m", "Initial commit from OpenHacker"], dest, { quiet: true });
159
-
160
- if (initialized) {
161
- console.log(`${MUTED}Initialized a git repository.${NC}`);
162
- } else {
163
- console.log(`${MUTED}Could not create the initial git commit \u2014 you can do it manually.${NC}`);
164
- }
165
- return initialized;
166
- }
167
-
168
- async function init(targetArg, { skipInstall = false, skipGit = false } = {}) {
169
- const template = await resolveTemplateDir();
170
- if (!(await exists(path.join(template, "agent", "agent.ts")))) {
171
- console.error(`${RED}Could not find the instance template at ${template}.${NC}`);
172
- console.error(`${MUTED}Set OPENHACKER_TEMPLATE_DIR to the template directory.${NC}`);
173
- process.exit(1);
174
- }
175
-
176
- const dest = path.resolve(process.cwd(), targetArg ?? "openhacker");
177
- if (await exists(dest)) {
178
- console.error(`${RED}Destination already exists: ${dest}${NC}`);
179
- process.exit(1);
180
- }
181
-
182
- console.log(`\n${MUTED}Creating OpenHacker instance in ${NC}${dest}`);
183
- await cp(template, dest, {
184
- recursive: true,
185
- filter: (src) => shouldCopyTemplatePath(src, template),
186
- });
187
-
188
- const pkgPath = path.join(dest, "package.json");
189
- const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
190
- pkg.name = packageNameFor(dest);
191
- pkg.private = true;
192
- await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
193
-
194
- // Write before git init so the initial commit doesn't include node_modules/.env.
195
- if (!(await exists(path.join(dest, ".gitignore")))) {
196
- await writeFile(path.join(dest, ".gitignore"), GITIGNORE);
197
- }
198
-
199
- const installed = skipInstall ? false : await installDependencies(dest);
200
- if (!skipGit) {
201
- await initGitRepo(dest);
202
- }
203
-
204
- console.log(`\n${ORANGE}\u2713${NC} OpenHacker instance ready.\n`);
205
- console.log("Next steps:\n");
206
- console.log(` cd ${path.relative(process.cwd(), dest) || "."}`);
207
- if (skipInstall || !installed) {
208
- console.log(" pnpm install");
209
- }
210
- console.log(" pnpm dev # run locally\n");
211
- console.log(`${MUTED}Deploy: push to a git repo and import it into Vercel (deploys as one project).`);
212
- console.log(`Add a Vercel KV / Upstash Redis integration to persist findings. See README.md.${NC}\n`);
213
- }
214
-
215
- function usage() {
216
- console.log("OpenHacker\n");
217
- console.log("Usage:");
218
- console.log(" openhacker [dir] Scaffold a deployable OpenHacker instance");
219
- console.log(" openhacker init [dir] Same as above");
220
- console.log(" openhacker --help Show help");
221
- console.log(" openhacker --version Show version\n");
222
- console.log("Options:");
223
- console.log(" --skip-install Don't run pnpm install");
224
- console.log(" --skip-git Don't create a git repository\n");
225
- }
226
-
227
- async function version() {
228
- const pkg = JSON.parse(await readFile(path.resolve(here, "../package.json"), "utf8"));
229
- console.log(pkg.version);
230
- }
231
-
232
- export async function run(args = process.argv.slice(2)) {
233
- const options = { skipInstall: false, skipGit: false };
234
- const positionals = [];
235
-
236
- for (const arg of args) {
237
- switch (arg) {
238
- case "--skip-install":
239
- options.skipInstall = true;
240
- break;
241
- case "--skip-git":
242
- options.skipGit = true;
243
- break;
244
- case "-h":
245
- case "--help":
246
- usage();
247
- return;
248
- case "-v":
249
- case "--version":
250
- await version();
251
- return;
252
- default:
253
- if (arg.startsWith("-")) {
254
- console.error(`${RED}Unknown option: ${arg}${NC}\n`);
255
- usage();
256
- process.exit(1);
257
- }
258
- positionals.push(arg);
259
- }
260
- }
261
-
262
- const [command, target, ...rest] = positionals;
263
-
264
- if (rest.length > 0) {
265
- console.error(`${RED}Too many arguments.${NC}\n`);
266
- usage();
267
- process.exit(1);
268
- }
269
-
270
- if (command === "init") {
271
- await init(target, options);
272
- return;
273
- }
274
-
275
- await init(command, options);
276
- }
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export const MESSAGE = "OpenHacker";