filegrc 0.3.2 → 0.3.4

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
@@ -34,7 +34,7 @@ npx filegrc prepare-audit audit-id
34
34
  npx filegrc evidence-packet --start 2026-01-01 --end 2026-06-30 --audit audit-id
35
35
  ```
36
36
 
37
- `filegrc serve --help` prints bind, port, environment, and safety options without starting the server. The editable server defaults to `127.0.0.1:8787`; set `FILEGRC_HOST`, `FILEGRC_PORT`, or the matching flags when needed.
37
+ `filegrc serve --help` prints bind, port, environment, and safety options without starting the server. The editable server defaults to `127.0.0.1:8787`; set `FILEGRC_HOST`, `FILEGRC_PORT`, or the matching flags when needed. In trunk mode, browser saves synchronize, commit, and push from the authoritative branch. Use `--allow-non-authoritative-writes` for local development in a task checkout; the override never commits or pushes.
38
38
 
39
39
  `filegrc setup` provides the headless equivalent of browser onboarding. Run it without arguments for guided terminal setup, or pass all initial service-boundary fields and a management program goal as flags or a JSON payload. Add `--preview` to validate and inspect the planned service and workspace writes without saving. Add `--summary --json` for compact agent output. Selecting Type 1 or Type 2 updates the workspace goal and selected systems. Setup does not select framework records, link controls, create evidence, or create an audit record.
40
40
 
@@ -54,7 +54,7 @@ Use `filegrc attach <evidence-id> <source-file>` to copy a fixed evidence file u
54
54
 
55
55
  Use `filegrc detach <evidence-id> <attachment-name> --yes` for explicit removal. Evidence records with linked local attachments cannot be deleted.
56
56
 
57
- The package requires Node.js 20 or newer. It uses Git for authors, commit timestamps, messages, diffs, and revisions. Browser commits are explicit. Without a remote they remain local; with a remote the browser commits and pushes together. Browser pulls use rebase. Agents and terminal users use Git directly; the filegrc CLI does not wrap pull, commit, or push.
57
+ The package requires Node.js 20 or newer. It uses Git for authors, commit timestamps, messages, diffs, and revisions. New workspaces use trunk mode, which fetches and fast-forwards before each browser mutation, validates and commits the saved change, then pushes. Existing settings without `repositoryMode` keep manual browser Git behavior. Agents and terminal users use Git directly; the filegrc CLI does not wrap pull, commit, or push.
58
58
 
59
59
  The editable server has no authentication and binds to loopback by default. Put it behind trusted authentication before exposing it on a network, or publish the read-only static build.
60
60
 
package/model/v1.json CHANGED
@@ -104,17 +104,20 @@
104
104
  "pluralTitle": "Renderer settings",
105
105
  "group": "repository",
106
106
  "singleton": "renderer.json",
107
- "description": "Optional local interface settings, including onboarding visibility and manually completed Step pages. Renderer settings are not required for SOC 2 and do not change compliance records.",
107
+ "description": "Optional local interface and browser repository settings, including onboarding visibility, manually completed Step pages, and authoritative-branch synchronization. Renderer settings are not required for SOC 2 and do not change compliance records.",
108
108
  "guidance": {
109
- "policyBasis": "Renderer settings are a filegrc convenience, not a SOC 2 requirement, control, audit record, or substitute for evidence.",
110
- "cadence": "Change it when the team wants to rerun or suppress an optional renderer workflow, then review and commit the resulting diff."
109
+ "policyBasis": "Renderer settings are a filegrc convenience, not a SOC 2 requirement, control, audit record, or substitute for evidence. Record lifecycle status represents approval; Git branches do not.",
110
+ "cadence": "Change it when the team wants to rerun or suppress an optional renderer workflow or change repository synchronization. Existing workspaces without repositoryMode keep manual browser Git behavior."
111
111
  },
112
112
  "required": ["showOnboarding"],
113
113
  "fields": {
114
114
  "showOnboarding": {"type":"boolean","label":"Show onboarding"},
115
- "completedStagePageIds": {"type":"array","items":"string","label":"Manually completed program pages"}
115
+ "completedStagePageIds": {"type":"array","items":"string","label":"Manually completed program pages"},
116
+ "repositoryMode": {"type":"enum","values":["trunk","manual"],"label":"Repository mode"},
117
+ "authoritativeBranch": {"type":"string","format":"git-name","label":"Authoritative branch (defaults to main)"},
118
+ "repositoryRemote": {"type":"string","format":"git-name","label":"Repository remote (defaults to origin)"}
116
119
  },
117
- "listFields": ["title","showOnboarding","completedStagePageIds"]
120
+ "listFields": ["title","showOnboarding","repositoryMode","authoritativeBranch","repositoryRemote","completedStagePageIds"]
118
121
  },
119
122
  "person": {
120
123
  "title": "Person",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filegrc",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Zero-dependency Git-native GRC engine",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/build.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { FAVICON_PNG } from "./favicon.js";
3
+ import { FAVICON_PNG, LOGO_MARK_PNG } from "./favicon.js";
4
4
  import { resolveWorkspacePath, resolveWorkspaceRoot } from "./paths.js";
5
5
  import { createAppState } from "./state.js";
6
6
  import { APP_SCRIPT, APP_STYLES, renderIndex } from "./web.js";
@@ -12,6 +12,7 @@ export async function buildWorkspace(input = process.cwd(), options = {}) {
12
12
  const paths = {
13
13
  html: resolveWorkspacePath(root, join(outputOption, "index.html")),
14
14
  favicon: resolveWorkspacePath(root, join(outputOption, "favicon.png")),
15
+ logoMark: resolveWorkspacePath(root, join(outputOption, "logo-mark-white.png")),
15
16
  script: resolveWorkspacePath(root, join(outputOption, "filegrc-app.js")),
16
17
  styles: resolveWorkspacePath(root, join(outputOption, "filegrc.css"))
17
18
  };
@@ -20,6 +21,7 @@ export async function buildWorkspace(input = process.cwd(), options = {}) {
20
21
  await Promise.all([
21
22
  writeFile(paths.html, renderIndex(state), "utf8"),
22
23
  writeFile(paths.favicon, FAVICON_PNG),
24
+ writeFile(paths.logoMark, LOGO_MARK_PNG),
23
25
  writeFile(paths.script, APP_SCRIPT, "utf8"),
24
26
  writeFile(paths.styles, APP_STYLES, "utf8")
25
27
  ]);
package/src/cli.js CHANGED
@@ -29,12 +29,14 @@ import { markdownEntries } from "./resource-markdown.js";
29
29
  import { searchResources } from "./search.js";
30
30
  import { serveWorkspace } from "./server.js";
31
31
  import { planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
32
+ import { printGithubStarMessage } from "./startup.js";
32
33
  import { createAppState } from "./state.js";
33
34
  import { currentCalendarDate } from "./time.js";
34
35
  import { validateWorkspace } from "./validate.js";
35
36
  import { loadWorkspace } from "./workspace.js";
36
37
 
37
38
  const BOOLEAN_FLAGS = new Set([
39
+ "allow-non-authoritative-writes",
38
40
  "check-docs",
39
41
  "complete",
40
42
  "current",
@@ -62,15 +64,22 @@ export async function runCli(argv = process.argv.slice(2)) {
62
64
  if (command === "serve") {
63
65
  const result = await serveWorkspace(positionals[0] ?? root, {
64
66
  host: flags.host ?? process.env.FILEGRC_HOST,
65
- port: flags.port ?? process.env.FILEGRC_PORT
67
+ port: flags.port ?? process.env.FILEGRC_PORT,
68
+ allowNonAuthoritativeWrites: flags["allow-non-authoritative-writes"] === true
66
69
  });
67
- console.log(`filegrc workspace: ${result.url}`);
68
- console.log(`Data: ${result.root}/data`);
69
- return await new Promise((resolvePromise) => {
70
- const stop = () => result.server.close(resolvePromise);
70
+ const stopped = new Promise((resolvePromise) => {
71
+ const stop = () => {
72
+ process.removeListener("SIGINT", stop);
73
+ process.removeListener("SIGTERM", stop);
74
+ result.server.close(resolvePromise);
75
+ };
71
76
  process.once("SIGINT", stop);
72
77
  process.once("SIGTERM", stop);
73
78
  });
79
+ console.log(`filegrc workspace: ${result.url}`);
80
+ console.log(`Data: ${result.root}/data`);
81
+ printGithubStarMessage();
82
+ return await stopped;
74
83
  }
75
84
  if (command === "setup") {
76
85
  const payload = positionals[0] ? await readSetupPayload(positionals[0]) : {};
@@ -676,7 +685,7 @@ function printHelp() {
676
685
  console.log(`filegrc - Git-native GRC workspace
677
686
 
678
687
  Usage:
679
- filegrc serve [root] [--host 127.0.0.1] [--port 8787]
688
+ filegrc serve [root] [--host 127.0.0.1] [--port 8787] [--allow-non-authoritative-writes]
680
689
  filegrc setup [setup.json|-] [setup options] [--draft] [--preview] [--summary] [--json]
681
690
  filegrc build [root] [--output .filegrc/site]
682
691
  filegrc validate [root] [--json]
@@ -713,12 +722,15 @@ All commands accept --root <workspace>. Writes never create Git commits.`);
713
722
  function printCommandHelp(command) {
714
723
  if (command === "serve") {
715
724
  console.log(`Usage:
716
- filegrc serve [root] [--host address] [--port number]
725
+ filegrc serve [root] [--host address] [--port number] [--allow-non-authoritative-writes]
717
726
 
718
727
  Options:
719
728
  --host <address> Bind address. Defaults to FILEGRC_HOST or 127.0.0.1.
720
729
  --port <number> Port. Defaults to FILEGRC_PORT or 8787. Use 0 for an available port.
721
730
  --root <path> Workspace path when no positional root is given.
731
+ --allow-non-authoritative-writes
732
+ Allow local browser writes from a task checkout. This explicit
733
+ development override never commits or pushes.
722
734
  --help Show this help without starting the server.
723
735
 
724
736
  Safety:
package/src/favicon.js CHANGED
@@ -1,109 +1,4 @@
1
- import { deflateSync } from "node:zlib";
1
+ import { readFileSync } from "node:fs";
2
2
 
3
- const SIZE = 64;
4
- const WHITE = [248, 249, 255, 255];
5
-
6
- export const FAVICON_PNG = createFavicon();
7
-
8
- function createFavicon() {
9
- const pixels = Buffer.alloc(SIZE * SIZE * 4);
10
-
11
- for (let y = 0; y < SIZE; y += 1) {
12
- for (let x = 0; x < SIZE; x += 1) {
13
- if (!insideRoundedSquare(x, y, 11)) continue;
14
- setPixel(pixels, x, y, backgroundColor(x, y));
15
- }
16
- }
17
-
18
- drawStroke(pixels, [
19
- [18, 8],
20
- [39, 8],
21
- [51, 20],
22
- [51, 50],
23
- [50, 53],
24
- [47, 55],
25
- [17, 55],
26
- [14, 54],
27
- [12, 51],
28
- [12, 13],
29
- [14, 10],
30
- [18, 8]
31
- ], 2.3, WHITE);
32
- drawStroke(pixels, [[39, 8], [39, 20], [51, 20]], 2.3, WHITE);
33
-
34
- const rows = Buffer.alloc((SIZE * 4 + 1) * SIZE);
35
- for (let y = 0; y < SIZE; y += 1) {
36
- const rowOffset = y * (SIZE * 4 + 1);
37
- rows[rowOffset] = 0;
38
- pixels.copy(rows, rowOffset + 1, y * SIZE * 4, (y + 1) * SIZE * 4);
39
- }
40
-
41
- const header = Buffer.alloc(13);
42
- header.writeUInt32BE(SIZE, 0);
43
- header.writeUInt32BE(SIZE, 4);
44
- header.set([8, 6, 0, 0, 0], 8);
45
-
46
- return Buffer.concat([
47
- Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
48
- pngChunk("IHDR", header),
49
- pngChunk("IDAT", deflateSync(rows)),
50
- pngChunk("IEND", Buffer.alloc(0))
51
- ]);
52
- }
53
-
54
- function insideRoundedSquare(x, y, radius) {
55
- const cornerX = x < radius ? radius - 1 : x >= SIZE - radius ? SIZE - radius : x;
56
- const cornerY = y < radius ? radius - 1 : y >= SIZE - radius ? SIZE - radius : y;
57
- return Math.hypot(x - cornerX, y - cornerY) <= radius;
58
- }
59
-
60
- function backgroundColor(x, y) {
61
- const progress = Math.min(1, (x + y) / ((SIZE - 1) * 1.2));
62
- return [0, 0, Math.round(112 + (53 - 112) * progress), 255];
63
- }
64
-
65
- function drawStroke(pixels, points, radius, color) {
66
- for (let y = 0; y < SIZE; y += 1) {
67
- for (let x = 0; x < SIZE; x += 1) {
68
- const onStroke = points.slice(1).some((point, index) => (
69
- distanceToSegment(x, y, points[index], point) <= radius
70
- ));
71
- if (onStroke) setPixel(pixels, x, y, color);
72
- }
73
- }
74
- }
75
-
76
- function distanceToSegment(x, y, start, end) {
77
- const dx = end[0] - start[0];
78
- const dy = end[1] - start[1];
79
- const lengthSquared = dx * dx + dy * dy;
80
- const progress = lengthSquared
81
- ? Math.max(0, Math.min(1, ((x - start[0]) * dx + (y - start[1]) * dy) / lengthSquared))
82
- : 0;
83
- return Math.hypot(x - (start[0] + progress * dx), y - (start[1] + progress * dy));
84
- }
85
-
86
- function setPixel(pixels, x, y, color) {
87
- pixels.set(color, (y * SIZE + x) * 4);
88
- }
89
-
90
- function pngChunk(type, data) {
91
- const typeBuffer = Buffer.from(type, "ascii");
92
- const chunk = Buffer.alloc(data.length + 12);
93
- chunk.writeUInt32BE(data.length, 0);
94
- typeBuffer.copy(chunk, 4);
95
- data.copy(chunk, 8);
96
- chunk.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), data.length + 8);
97
- return chunk;
98
- }
99
-
100
- function crc32(buffer) {
101
- let value = 0xffffffff;
102
- for (const byte of buffer) {
103
- value ^= byte;
104
- for (let bit = 0; bit < 8; bit += 1) {
105
- value = (value >>> 1) ^ (value & 1 ? 0xedb88320 : 0);
106
- }
107
- }
108
- return (value ^ 0xffffffff) >>> 0;
109
- }
3
+ export const FAVICON_PNG = readFileSync(new URL("./favicon.png", import.meta.url));
4
+ export const LOGO_MARK_PNG = readFileSync(new URL("./logo-mark-white.png", import.meta.url));
Binary file
@@ -0,0 +1,16 @@
1
+ export function isSafeGitName(value) {
2
+ const segments = value.split("/");
3
+ return Boolean(value)
4
+ && value !== "@"
5
+ && value !== "HEAD"
6
+ && !value.startsWith("-")
7
+ && !value.includes("..")
8
+ && !value.includes("@{")
9
+ && !/[\s~^:?*[\]\\\u0000-\u001f\u007f]/.test(value)
10
+ && segments.every((segment) => (
11
+ segment
12
+ && !segment.startsWith(".")
13
+ && !segment.endsWith(".")
14
+ && !segment.endsWith(".lock")
15
+ ));
16
+ }