forgedock 1.0.11 → 1.0.13

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.
Files changed (2) hide show
  1. package/bin/forgedock.mjs +130 -16
  2. package/package.json +1 -1
package/bin/forgedock.mjs CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  readFileSync,
10
10
  writeFileSync,
11
11
  renameSync,
12
+ copyFileSync,
12
13
  } from "fs";
13
14
  import { execSync } from "child_process";
14
15
  import readline from "readline";
@@ -36,6 +37,42 @@ const YELLOW = "\x1b[33m";
36
37
  const CYAN = "\x1b[36m";
37
38
  const RED = "\x1b[31m";
38
39
  const RESET = "\x1b[0m";
40
+ const DIM = "\x1b[2m";
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Splash — the ForgeDock wordmark shown at startup
44
+ // ---------------------------------------------------------------------------
45
+
46
+ const LOGO_LINES = [
47
+ " ╔═╗╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗╔═╗╦╔═",
48
+ " ╠╣ ║ ║╠╦╝║ ╦║╣ ║║║ ║║ ╠╩╗",
49
+ " ╚ ╚═╝╩╚═╚═╝╚═╝═╩╝╚═╝╚═╝╩ ╩",
50
+ ];
51
+
52
+ function getVersion() {
53
+ try {
54
+ const pkg = JSON.parse(
55
+ readFileSync(join(FORGE_HOME, "package.json"), "utf-8"),
56
+ );
57
+ return pkg.version || "";
58
+ } catch {
59
+ return "";
60
+ }
61
+ }
62
+
63
+ function splash() {
64
+ const version = getVersion();
65
+ console.log("");
66
+ for (const line of LOGO_LINES) {
67
+ console.log(`${BOLD}${CYAN}${line}${RESET}`);
68
+ }
69
+ console.log("");
70
+ console.log(` ${DIM}GitHub as a knowledge graph for AI agents${RESET}`);
71
+ if (version) {
72
+ console.log(` ${DIM}v${version}${RESET}`);
73
+ }
74
+ console.log("");
75
+ }
39
76
 
40
77
  /**
41
78
  * Prompt the user for a yes/no confirmation.
@@ -55,11 +92,34 @@ async function confirmOverwrite(question) {
55
92
  });
56
93
  rl.question(question, (answer) => {
57
94
  rl.close();
58
- resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
95
+ resolve(
96
+ answer.trim().toLowerCase() === "y" ||
97
+ answer.trim().toLowerCase() === "yes",
98
+ );
59
99
  });
60
100
  });
61
101
  }
62
102
 
103
+ /**
104
+ * Returns true if `dir` is inside a git work tree. Used to avoid generating
105
+ * forge.yaml in non-project directories — e.g. when npx defaults cwd to a
106
+ * system folder (C:\Windows) because the launching shell sat on an
107
+ * unsupported UNC/WSL path. The command is a fixed string with no
108
+ * interpolated input, so there is no shell-injection surface (ref #151).
109
+ */
110
+ function isGitWorkTree(dir) {
111
+ try {
112
+ const out = execSync("git rev-parse --is-inside-work-tree", {
113
+ cwd: dir,
114
+ stdio: ["ignore", "pipe", "ignore"],
115
+ encoding: "utf-8",
116
+ });
117
+ return out.trim() === "true";
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
63
123
  async function findMarkdownFiles(dir) {
64
124
  const results = [];
65
125
  const entries = await readdir(dir, { withFileTypes: true });
@@ -110,10 +170,17 @@ async function install() {
110
170
  updated++;
111
171
  }
112
172
  } else {
113
- console.log(
114
- ` ${YELLOW}WARNING${RESET}: ${rel} is a regular file — skipping (remove it manually to let ForgeDock manage it)`,
115
- );
116
- skipped++;
173
+ // Existing regular file — a ForgeDock-shipped command copied by a prior
174
+ // install (common on Windows, where symlinks aren't permitted). It is
175
+ // ForgeDock-managed (we only iterate shipped command names), so refresh
176
+ // it: skip if already current, otherwise overwrite with a fresh copy.
177
+ if (readFileSync(file, "utf-8") === readFileSync(target, "utf-8")) {
178
+ skipped++;
179
+ } else {
180
+ copyFileSync(file, target);
181
+ console.log(` ${YELLOW}Updated${RESET}: ${rel}`);
182
+ updated++;
183
+ }
117
184
  }
118
185
  } catch (err) {
119
186
  if (err.code !== "ENOENT") {
@@ -122,9 +189,20 @@ async function install() {
122
189
  );
123
190
  throw err;
124
191
  }
125
- // Doesn't exist — create symlink
126
- await symlink(file, target);
127
- console.log(` ${GREEN}Installed${RESET}: ${rel}`);
192
+ // Doesn't exist — create it. Prefer a symlink; fall back to a copy on
193
+ // systems where symlink creation is not permitted (e.g. Windows without
194
+ // Developer Mode / admin), which would otherwise throw EPERM. See #587.
195
+ try {
196
+ await symlink(file, target);
197
+ console.log(` ${GREEN}Installed${RESET}: ${rel}`);
198
+ } catch (linkErr) {
199
+ if (linkErr.code === "EPERM" || linkErr.code === "EACCES") {
200
+ copyFileSync(file, target);
201
+ console.log(` ${GREEN}Installed${RESET} (copy): ${rel}`);
202
+ } else {
203
+ throw linkErr;
204
+ }
205
+ }
128
206
  installed++;
129
207
  }
130
208
  }
@@ -159,10 +237,24 @@ async function install() {
159
237
  );
160
238
  console.log("");
161
239
 
162
- // Auto-generate forge.yaml if missing — no second command needed
163
- const forgeYamlPath = join(process.cwd(), "forge.yaml");
240
+ // Auto-generate forge.yaml if missing — no second command needed.
241
+ // Only do this inside a real git project; otherwise cwd may be a system or
242
+ // read-only directory (e.g. C:\Windows when npx is launched from a WSL path),
243
+ // and writing forge.yaml there would crash. See #585.
244
+ const cwd = process.cwd();
245
+ const forgeYamlPath = join(cwd, "forge.yaml");
164
246
  if (!existsSync(forgeYamlPath)) {
165
- await init(true);
247
+ if (isGitWorkTree(cwd)) {
248
+ await init(true);
249
+ } else {
250
+ console.log(
251
+ ` ${YELLOW}Skipping forge.yaml generation${RESET} — current directory is not a git project.`,
252
+ );
253
+ console.log(
254
+ ` Run ${CYAN}npx forgedock init${RESET} inside your project directory to generate it.`,
255
+ );
256
+ console.log("");
257
+ }
166
258
  }
167
259
  }
168
260
 
@@ -180,14 +272,23 @@ async function uninstall() {
180
272
 
181
273
  try {
182
274
  const stats = await lstat(target);
275
+ const { unlink } = await import("fs/promises");
183
276
  if (stats.isSymbolicLink()) {
184
277
  const current = await readlink(target);
185
278
  if (current === file) {
186
- const { unlink } = await import("fs/promises");
187
279
  await unlink(target);
188
280
  console.log(` ${RED}Removed${RESET}: ${rel}`);
189
281
  removed++;
190
282
  }
283
+ } else if (
284
+ readFileSync(file, "utf-8") === readFileSync(target, "utf-8")
285
+ ) {
286
+ // Regular file installed by ForgeDock in copy mode (content matches the
287
+ // shipped command). Safe to remove. Content-differing files are left
288
+ // in place in case the user edited them.
289
+ await unlink(target);
290
+ console.log(` ${RED}Removed${RESET}: ${rel}`);
291
+ removed++;
191
292
  }
192
293
  } catch (err) {
193
294
  if (err.code !== "ENOENT") {
@@ -511,15 +612,15 @@ project:
511
612
  name: "${projectName}"
512
613
  owner: "${owner}"
513
614
  repo: "${repo}"
514
- description: "${description.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"
615
+ description: "${description.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"
515
616
 
516
617
  # =============================================================================
517
618
  # PATHS (REQUIRED)
518
619
  # =============================================================================
519
620
 
520
621
  paths:
521
- root: "${cwd.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"
522
- worktree_base: "${worktreeBase.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"
622
+ root: "${cwd.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"
623
+ worktree_base: "${worktreeBase.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"
523
624
 
524
625
  # =============================================================================
525
626
  # BRANCHES (REQUIRED)
@@ -583,7 +684,18 @@ branches:
583
684
  # - '"status": "ok"'
584
685
  `;
585
686
 
586
- writeFileSync(outputPath, content, "utf-8");
687
+ try {
688
+ writeFileSync(outputPath, content, "utf-8");
689
+ } catch (err) {
690
+ console.error("");
691
+ console.error(
692
+ ` ${RED}Error${RESET}: Could not write forge.yaml to ${outputPath} — ${err.code ?? err.message}`,
693
+ );
694
+ console.error(
695
+ ` Run ${CYAN}npx forgedock init${RESET} from a writable project directory.`,
696
+ );
697
+ process.exit(1);
698
+ }
587
699
 
588
700
  console.log(` ${GREEN}Created${RESET}: forge.yaml`);
589
701
  console.log("");
@@ -641,6 +753,8 @@ function help() {
641
753
  console.log("");
642
754
  }
643
755
 
756
+ splash();
757
+
644
758
  switch (command) {
645
759
  case "install":
646
760
  await install();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "GitHub as a knowledge graph for AI agents. Autonomous dev pipeline for Claude Code.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"