@jobshimo/browser-link 0.8.0 → 0.8.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.
Files changed (38) hide show
  1. package/dist/agent-instructions/content.d.ts +15 -2
  2. package/dist/agent-instructions/content.js +29 -4
  3. package/dist/agent-instructions/content.js.map +1 -1
  4. package/dist/agent-instructions/errors.d.ts +38 -0
  5. package/dist/agent-instructions/errors.js +59 -0
  6. package/dist/agent-instructions/errors.js.map +1 -0
  7. package/dist/agent-instructions/file-ops.d.ts +8 -2
  8. package/dist/agent-instructions/file-ops.js +119 -26
  9. package/dist/agent-instructions/file-ops.js.map +1 -1
  10. package/dist/agent-instructions/index.d.ts +28 -0
  11. package/dist/agent-instructions/index.js +69 -3
  12. package/dist/agent-instructions/index.js.map +1 -1
  13. package/dist/agent-instructions/types.d.ts +5 -2
  14. package/dist/bridge/server.js +30 -28
  15. package/dist/bridge/server.js.map +1 -1
  16. package/dist/commands/doctor.js +9 -4
  17. package/dist/commands/doctor.js.map +1 -1
  18. package/dist/commands/instructions.d.ts +7 -1
  19. package/dist/commands/instructions.js +52 -27
  20. package/dist/commands/instructions.js.map +1 -1
  21. package/dist/commands/updates.js +5 -12
  22. package/dist/commands/updates.js.map +1 -1
  23. package/dist/extension/manifest.json +1 -1
  24. package/dist/ui/screens/agent-instructions.js +25 -6
  25. package/dist/ui/screens/agent-instructions.js.map +1 -1
  26. package/dist/utils/semver.d.ts +14 -0
  27. package/dist/utils/semver.js +32 -0
  28. package/dist/utils/semver.js.map +1 -0
  29. package/package.json +1 -1
  30. package/dist/agent-instructions/claude.d.ts +0 -2
  31. package/dist/agent-instructions/claude.js +0 -29
  32. package/dist/agent-instructions/claude.js.map +0 -1
  33. package/dist/agent-instructions/copilot.d.ts +0 -2
  34. package/dist/agent-instructions/copilot.js +0 -31
  35. package/dist/agent-instructions/copilot.js.map +0 -1
  36. package/dist/agent-instructions/opencode.d.ts +0 -2
  37. package/dist/agent-instructions/opencode.js +0 -30
  38. package/dist/agent-instructions/opencode.js.map +0 -1
@@ -21,8 +21,21 @@ export declare const BEGIN_RE: RegExp;
21
21
  export declare const END_RE: RegExp;
22
22
  /** Full fenced block to write into the file, including a trailing newline.
23
23
  * The line between markers carries the managed-by stamp so a user reading
24
- * the file knows it is auto-generated and how to refresh it. */
25
- export declare function block(version?: string): string;
24
+ * the file knows it is auto-generated and how to refresh it.
25
+ *
26
+ * `eol` controls the line separator. Default is LF; pass CRLF to match a
27
+ * Windows-edited file's dominant line ending. The body() helper composes
28
+ * its own lines with `\n`; we replace them when joining so the produced
29
+ * block is uniformly LF or CRLF — never mixed. */
30
+ export declare function block(version?: string, eol?: '\n' | '\r\n'): string;
31
+ /** Count occurrences of the BEGIN marker in `text`. The base BEGIN_RE has
32
+ * no `g` flag (it's used for single-span detection); this helper compiles
33
+ * the same pattern with `g` so duplicates can be diagnosed. */
34
+ export declare function countBeginMarkers(text: string): number;
35
+ /** Pick the dominant line ending of `text`. CRLF wins only when it is the
36
+ * majority of newline sequences; otherwise LF. Used by installAt to keep
37
+ * Windows-edited files from acquiring a mixed-EOL block. */
38
+ export declare function detectEol(text: string): '\n' | '\r\n';
26
39
  /** Locate the block boundaries in the given file text. Returns null when
27
40
  * either marker is missing. Used by detect/install/uninstall to act on the
28
41
  * exact span the installer owns. */
@@ -60,9 +60,14 @@ function body() {
60
60
  }
61
61
  /** Full fenced block to write into the file, including a trailing newline.
62
62
  * The line between markers carries the managed-by stamp so a user reading
63
- * the file knows it is auto-generated and how to refresh it. */
64
- export function block(version = VERSION) {
65
- return [
63
+ * the file knows it is auto-generated and how to refresh it.
64
+ *
65
+ * `eol` controls the line separator. Default is LF; pass CRLF to match a
66
+ * Windows-edited file's dominant line ending. The body() helper composes
67
+ * its own lines with `\n`; we replace them when joining so the produced
68
+ * block is uniformly LF or CRLF — never mixed. */
69
+ export function block(version = VERSION, eol = '\n') {
70
+ const parts = [
66
71
  beginMarker(version),
67
72
  `<!-- managed-by: browser-link v${version}; do not edit between markers; run \`browser-link instructions install\` to refresh. -->`,
68
73
  '',
@@ -70,7 +75,27 @@ export function block(version = VERSION) {
70
75
  '',
71
76
  END_MARKER,
72
77
  '',
73
- ].join('\n');
78
+ ];
79
+ if (eol === '\n')
80
+ return parts.join('\n');
81
+ // body() returned a multi-line string joined with \n; flip each \n to \r\n.
82
+ return parts.map((p) => p.replace(/\n/g, '\r\n')).join('\r\n');
83
+ }
84
+ /** Count occurrences of the BEGIN marker in `text`. The base BEGIN_RE has
85
+ * no `g` flag (it's used for single-span detection); this helper compiles
86
+ * the same pattern with `g` so duplicates can be diagnosed. */
87
+ export function countBeginMarkers(text) {
88
+ return (text.match(/<!--\s*browser-link:instructions:begin(?:\s+v\d+\.\d+\.\d+)?\s*-->/g) ?? [])
89
+ .length;
90
+ }
91
+ /** Pick the dominant line ending of `text`. CRLF wins only when it is the
92
+ * majority of newline sequences; otherwise LF. Used by installAt to keep
93
+ * Windows-edited files from acquiring a mixed-EOL block. */
94
+ export function detectEol(text) {
95
+ const crlf = (text.match(/\r\n/g) ?? []).length;
96
+ const lfTotal = (text.match(/\n/g) ?? []).length;
97
+ const loneLf = lfTotal - crlf;
98
+ return crlf > loneLf && crlf > 0 ? '\r\n' : '\n';
74
99
  }
75
100
  export function findBlockSpan(text) {
76
101
  const begin = BEGIN_RE.exec(text);
@@ -1 +1 @@
1
- {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/agent-instructions/content.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC;;;;;;;;;;GAUG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,sCAAsC,CAAC;AACnE,MAAM,CAAC,MAAM,UAAU,GAAG,wCAAwC,CAAC;AAEnE,MAAM,UAAU,WAAW,CAAC,OAAO,GAAG,OAAO;IAC3C,OAAO,GAAG,YAAY,KAAK,OAAO,MAAM,CAAC;AAC3C,CAAC;AAED;;;;yCAIyC;AACzC,MAAM,CAAC,MAAM,QAAQ,GACnB,gFAAgF,CAAC;AACnF,MAAM,CAAC,MAAM,MAAM,GAAG,4CAA4C,CAAC;AAEnE;6EAC6E;AAC7E,SAAS,IAAI;IACX,OAAO;QACL,wCAAwC;QACxC,EAAE;QACF,sEAAsE;QACtE,qEAAqE;QACrE,kEAAkE;QAClE,qEAAqE;QACrE,aAAa;QACb,EAAE;QACF,uCAAuC;QACvC,EAAE;QACF,uEAAuE;QACvE,yEAAyE;QACzE,gEAAgE;QAChE,qEAAqE;QACrE,6DAA6D;QAC7D,0EAA0E;QAC1E,wEAAwE;QACxE,uEAAuE;QACvE,uEAAuE;QACvE,uDAAuD;QACvD,sEAAsE;QACtE,+DAA+D;QAC/D,kEAAkE;QAClE,wBAAwB;QACxB,EAAE;QACF,6BAA6B;QAC7B,EAAE;QACF,oEAAoE;QACpE,sEAAsE;QACtE,0DAA0D;KAC3D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;gEAEgE;AAChE,MAAM,UAAU,KAAK,CAAC,OAAO,GAAG,OAAO;IACrC,OAAO;QACL,WAAW,CAAC,OAAO,CAAC;QACpB,kCAAkC,OAAO,0FAA0F;QACnI,EAAE;QACF,IAAI,EAAE;QACN,EAAE;QACF,UAAU;QACV,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAWD,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACjD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IAChD,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,sBAAsB,GAAG,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC;IACtD,MAAM,oBAAoB,GAAG,sBAAsB,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACpE,6DAA6D;IAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,oBAAoB,CAAC,CAAC;IAC5C,MAAM,QAAQ,GAAG,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,oBAAoB,GAAG,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC;IACrF,OAAO;QACL,UAAU,EAAE,KAAK,CAAC,KAAK;QACvB,QAAQ;QACR,gBAAgB,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO,IAAI,IAAI;KAChD,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/agent-instructions/content.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Typed errors thrown by the agent-instructions file operations. Surfaced
3
+ * in the Ink screen as red banners; the CLI prints them straight to stderr.
4
+ * Keeping them in their own module avoids cycles between file-ops and the
5
+ * UI / command layers that catch them.
6
+ */
7
+ /** Thrown when the target file is a symlink. We refuse to write through
8
+ * symlinks because the user's intent is ambiguous: editing the target
9
+ * may not be what they meant when they installed the symlink. The error
10
+ * message embeds a paste-ready block so the user can install it manually
11
+ * in the resolved target. */
12
+ export declare class SymlinkRefusedError extends Error {
13
+ readonly source: string;
14
+ readonly target: string;
15
+ readonly blockContent: string;
16
+ constructor(source: string, target: string, blockContent: string);
17
+ }
18
+ /** Thrown when the target file contains more than one BEGIN marker. The
19
+ * installer cannot know which span to refresh; the user must resolve the
20
+ * duplication by hand before install/uninstall can proceed. */
21
+ export declare class CorruptBlockError extends Error {
22
+ readonly filePath: string;
23
+ readonly reason: 'multiple-begin-markers';
24
+ constructor(filePath: string, reason: 'multiple-begin-markers');
25
+ }
26
+ /** Thrown when a write would land outside the resolved `$HOME` directory
27
+ * and the caller did not opt into the override. The block is meant for
28
+ * the user's own dotfile area; refusing to touch anything outside that
29
+ * tree is the safer failure mode (a tampered `HOME` or a typo'd config
30
+ * path should not let the installer scribble in `/etc` or `C:\Windows`).
31
+ * Clients with an explicit env-var override (e.g. Copilot's `COPILOT_HOME`)
32
+ * pass `{ allowOutsideHome: true }` through `installAt` / `uninstallAt`
33
+ * — that is the user opting in. */
34
+ export declare class OutsideHomeError extends Error {
35
+ readonly filePath: string;
36
+ readonly home: string;
37
+ constructor(filePath: string, home: string);
38
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Typed errors thrown by the agent-instructions file operations. Surfaced
3
+ * in the Ink screen as red banners; the CLI prints them straight to stderr.
4
+ * Keeping them in their own module avoids cycles between file-ops and the
5
+ * UI / command layers that catch them.
6
+ */
7
+ /** Thrown when the target file is a symlink. We refuse to write through
8
+ * symlinks because the user's intent is ambiguous: editing the target
9
+ * may not be what they meant when they installed the symlink. The error
10
+ * message embeds a paste-ready block so the user can install it manually
11
+ * in the resolved target. */
12
+ export class SymlinkRefusedError extends Error {
13
+ source;
14
+ target;
15
+ blockContent;
16
+ constructor(source, target, blockContent) {
17
+ super(`${source} is a symlink to ${target}. Refusing to write through it. ` +
18
+ `Paste the block below into ${target} manually:\n\n${blockContent}`);
19
+ this.source = source;
20
+ this.target = target;
21
+ this.blockContent = blockContent;
22
+ this.name = 'SymlinkRefusedError';
23
+ }
24
+ }
25
+ /** Thrown when the target file contains more than one BEGIN marker. The
26
+ * installer cannot know which span to refresh; the user must resolve the
27
+ * duplication by hand before install/uninstall can proceed. */
28
+ export class CorruptBlockError extends Error {
29
+ filePath;
30
+ reason;
31
+ constructor(filePath, reason) {
32
+ super(`${filePath} contains multiple browser-link instruction blocks ` +
33
+ `(${reason}). Refusing to touch it — please resolve the duplication manually.`);
34
+ this.filePath = filePath;
35
+ this.reason = reason;
36
+ this.name = 'CorruptBlockError';
37
+ }
38
+ }
39
+ /** Thrown when a write would land outside the resolved `$HOME` directory
40
+ * and the caller did not opt into the override. The block is meant for
41
+ * the user's own dotfile area; refusing to touch anything outside that
42
+ * tree is the safer failure mode (a tampered `HOME` or a typo'd config
43
+ * path should not let the installer scribble in `/etc` or `C:\Windows`).
44
+ * Clients with an explicit env-var override (e.g. Copilot's `COPILOT_HOME`)
45
+ * pass `{ allowOutsideHome: true }` through `installAt` / `uninstallAt`
46
+ * — that is the user opting in. */
47
+ export class OutsideHomeError extends Error {
48
+ filePath;
49
+ home;
50
+ constructor(filePath, home) {
51
+ super(`Refusing to write ${filePath}: target is outside the user home directory ` +
52
+ `(${home}). Set the client-specific override env var (e.g. COPILOT_HOME) ` +
53
+ `if this path is intentional.`);
54
+ this.filePath = filePath;
55
+ this.home = home;
56
+ this.name = 'OutsideHomeError';
57
+ }
58
+ }
59
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/agent-instructions/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;6BAI6B;AAC7B,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAE1B;IACA;IACA;IAHlB,YACkB,MAAc,EACd,MAAc,EACd,YAAoB;QAEpC,KAAK,CACH,GAAG,MAAM,oBAAoB,MAAM,kCAAkC;YACnE,8BAA8B,MAAM,iBAAiB,YAAY,EAAE,CACtE,CAAC;QAPc,WAAM,GAAN,MAAM,CAAQ;QACd,WAAM,GAAN,MAAM,CAAQ;QACd,iBAAY,GAAZ,YAAY,CAAQ;QAMpC,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED;;+DAE+D;AAC/D,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAExB;IACA;IAFlB,YACkB,QAAgB,EAChB,MAAgC;QAEhD,KAAK,CACH,GAAG,QAAQ,qDAAqD;YAC9D,IAAI,MAAM,oEAAoE,CACjF,CAAC;QANc,aAAQ,GAAR,QAAQ,CAAQ;QAChB,WAAM,GAAN,MAAM,CAA0B;QAMhD,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED;;;;;;;mCAOmC;AACnC,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAEvB;IACA;IAFlB,YACkB,QAAgB,EAChB,IAAY;QAE5B,KAAK,CACH,qBAAqB,QAAQ,8CAA8C;YACzE,IAAI,IAAI,kEAAkE;YAC1E,8BAA8B,CACjC,CAAC;QAPc,aAAQ,GAAR,QAAQ,CAAQ;QAChB,SAAI,GAAJ,IAAI,CAAQ;QAO5B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF"}
@@ -1,8 +1,14 @@
1
1
  import type { InstructionsDetect } from './types.js';
2
+ /** Options for the write-side helpers. `allowOutsideHome` opts out of the
3
+ * outside-`$HOME` guard for clients that honour an explicit user-controlled
4
+ * override (e.g. Copilot CLI's `COPILOT_HOME`). */
5
+ export interface FileOpOptions {
6
+ allowOutsideHome?: boolean;
7
+ }
2
8
  export declare function detectAt(filePath: string): InstructionsDetect;
3
9
  /**
4
10
  * Insert or refresh the block in `filePath`. Returns a one-line description
5
11
  * of what changed, suitable for surfacing to the user.
6
12
  */
7
- export declare function installAt(filePath: string, displayName: string): string;
8
- export declare function uninstallAt(filePath: string, displayName: string): string;
13
+ export declare function installAt(filePath: string, displayName: string, options?: FileOpOptions): string;
14
+ export declare function uninstallAt(filePath: string, displayName: string, options?: FileOpOptions): string;
@@ -1,33 +1,103 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { dirname } from 'node:path';
1
+ import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, renameSync, unlinkSync, writeSync, } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { basename, dirname, join, resolve, sep } from 'node:path';
3
4
  import { VERSION } from '../version.js';
4
- import { block, findBlockSpan } from './content.js';
5
+ import { compareSemver } from '../utils/semver.js';
6
+ import { block, countBeginMarkers, detectEol, findBlockSpan } from './content.js';
7
+ import { CorruptBlockError, OutsideHomeError, SymlinkRefusedError } from './errors.js';
5
8
  /**
6
9
  * Shared file-level operations for the three agent-instructions installers.
7
10
  * Per-client modules only know which path their target file lives at; the
8
11
  * marker handling and the text splice are the same everywhere.
9
12
  */
10
- function compareSemver(a, b) {
11
- const pa = a.split('.').map(Number);
12
- const pb = b.split('.').map(Number);
13
- for (let i = 0; i < 3; i++) {
14
- const da = pa[i] ?? 0;
15
- const db = pb[i] ?? 0;
16
- if (da !== db)
17
- return da - db;
13
+ /** Atomic write via a sibling temp file + fsync + rename. Closes the
14
+ * TOCTOU window between existsSync()/stat and writeFileSync() that
15
+ * CodeQL flagged as js/file-system-race. Cleans up the temp file if
16
+ * the rename fails (or any earlier step throws). The 0o600 create-mode
17
+ * keeps the temp file private while it lives. */
18
+ function atomicWrite(filePath, content) {
19
+ const dir = dirname(filePath);
20
+ const tmp = join(dir, `.${basename(filePath)}.${process.pid}-${Date.now()}.tmp`);
21
+ let fd = null;
22
+ try {
23
+ fd = openSync(tmp, 'w', 0o600);
24
+ writeSync(fd, content, null, 'utf8');
25
+ fsyncSync(fd);
26
+ closeSync(fd);
27
+ fd = null;
28
+ renameSync(tmp, filePath);
18
29
  }
19
- return 0;
30
+ catch (err) {
31
+ if (fd !== null) {
32
+ try {
33
+ closeSync(fd);
34
+ }
35
+ catch {
36
+ /* swallow — we're already on the error path */
37
+ }
38
+ }
39
+ try {
40
+ unlinkSync(tmp);
41
+ }
42
+ catch {
43
+ /* swallow — the tmp may never have been created */
44
+ }
45
+ throw err;
46
+ }
47
+ }
48
+ /** Refuse to touch paths that land outside the user's home directory. The
49
+ * block we manage is meant for the user's own dotfiles; writing into
50
+ * `/etc`, `C:\Windows`, or anywhere unrelated is almost always a bug or a
51
+ * tampered config. `allowOutsideHome: true` skips the check — used when
52
+ * the user opted in through a client-specific env var (e.g. `COPILOT_HOME`).
53
+ * The home directory itself counts as "under" home (the equality branch). */
54
+ function assertUnderHome(filePath) {
55
+ const home = resolve(homedir());
56
+ const resolved = resolve(filePath);
57
+ if (resolved === home)
58
+ return;
59
+ if (resolved.startsWith(home + sep))
60
+ return;
61
+ throw new OutsideHomeError(filePath, home);
62
+ }
63
+ /** Throw SymlinkRefusedError if `filePath` is a symlink. existsSync follows
64
+ * symlinks, so callers must use this lstat-based check separately. */
65
+ function guardSymlink(filePath) {
66
+ let st;
67
+ try {
68
+ st = lstatSync(filePath);
69
+ }
70
+ catch {
71
+ return; // file does not exist — not our problem here
72
+ }
73
+ if (!st.isSymbolicLink())
74
+ return;
75
+ const rawTarget = readlinkSync(filePath);
76
+ // realpath gives us the absolute resolved path even when readlink returns
77
+ // something relative. Fall back to the raw readlink output if realpath
78
+ // throws (broken symlink) so the error message still names a target.
79
+ let resolved;
80
+ try {
81
+ resolved = realpathSync(filePath);
82
+ }
83
+ catch {
84
+ resolved = rawTarget;
85
+ }
86
+ throw new SymlinkRefusedError(filePath, resolved, block());
20
87
  }
21
88
  export function detectAt(filePath) {
22
89
  if (!existsSync(filePath)) {
23
90
  return { filePath, state: { kind: 'no-file' } };
24
91
  }
25
92
  const text = readFileSync(filePath, 'utf8');
93
+ if (countBeginMarkers(text) > 1) {
94
+ return { filePath, state: { kind: 'corrupt', reason: 'multiple-begin-markers' } };
95
+ }
26
96
  const span = findBlockSpan(text);
27
97
  if (!span) {
28
98
  return { filePath, state: { kind: 'not-installed' } };
29
99
  }
30
- const installedVersion = span.installedVersion ?? '0.0.0';
100
+ const installedVersion = span.installedVersion;
31
101
  const cmp = compareSemver(installedVersion, VERSION);
32
102
  return {
33
103
  filePath,
@@ -40,32 +110,50 @@ export function detectAt(filePath) {
40
110
  * Insert or refresh the block in `filePath`. Returns a one-line description
41
111
  * of what changed, suitable for surfacing to the user.
42
112
  */
43
- export function installAt(filePath, displayName) {
44
- const fresh = block();
113
+ export function installAt(filePath, displayName, options = {}) {
114
+ if (!options.allowOutsideHome)
115
+ assertUnderHome(filePath);
116
+ guardSymlink(filePath);
45
117
  if (!existsSync(filePath)) {
46
118
  mkdirSync(dirname(filePath), { recursive: true });
47
- writeFileSync(filePath, fresh, 'utf8');
119
+ // New file: portable markdown — LF by default. We do NOT pick CRLF on
120
+ // Windows because the content is going into a tool-managed .md file
121
+ // that may be synced to other OSes.
122
+ atomicWrite(filePath, block());
48
123
  return `Created ${filePath} and inserted the browser-link instructions block for ${displayName}.`;
49
124
  }
50
125
  const text = readFileSync(filePath, 'utf8');
126
+ if (countBeginMarkers(text) > 1) {
127
+ throw new CorruptBlockError(filePath, 'multiple-begin-markers');
128
+ }
129
+ const eol = detectEol(text);
130
+ const fresh = block(VERSION, eol);
51
131
  const span = findBlockSpan(text);
52
132
  if (!span) {
53
- // Append. Ensure a blank line separator from prior content.
54
- const separator = text.length === 0 || text.endsWith('\n\n') ? '' : text.endsWith('\n') ? '\n' : '\n\n';
55
- writeFileSync(filePath, text + separator + fresh, 'utf8');
133
+ // Append. Ensure a blank line separator from prior content, matching
134
+ // the file's dominant line ending.
135
+ const doubleEol = `${eol}${eol}`;
136
+ const separator = text.length === 0 || text.endsWith(doubleEol) ? '' : text.endsWith(eol) ? eol : doubleEol;
137
+ atomicWrite(filePath, text + separator + fresh);
56
138
  return `Appended the browser-link instructions block to ${filePath} (${displayName}).`;
57
139
  }
58
140
  const before = text.slice(0, span.startIndex);
59
141
  const after = text.slice(span.endIndex);
60
- writeFileSync(filePath, before + fresh + after, 'utf8');
142
+ atomicWrite(filePath, before + fresh + after);
61
143
  const prev = span.installedVersion ?? 'unversioned';
62
144
  return `Refreshed the browser-link instructions block in ${filePath} (${displayName}, was v${prev}).`;
63
145
  }
64
- export function uninstallAt(filePath, displayName) {
146
+ export function uninstallAt(filePath, displayName, options = {}) {
147
+ if (!options.allowOutsideHome)
148
+ assertUnderHome(filePath);
65
149
  if (!existsSync(filePath)) {
66
150
  return `No ${displayName} instructions file at ${filePath}; nothing to remove.`;
67
151
  }
152
+ guardSymlink(filePath);
68
153
  const text = readFileSync(filePath, 'utf8');
154
+ if (countBeginMarkers(text) > 1) {
155
+ throw new CorruptBlockError(filePath, 'multiple-begin-markers');
156
+ }
69
157
  const span = findBlockSpan(text);
70
158
  if (!span) {
71
159
  return `browser-link instructions block was not present in ${filePath}; nothing to remove.`;
@@ -73,11 +161,16 @@ export function uninstallAt(filePath, displayName) {
73
161
  const before = text.slice(0, span.startIndex);
74
162
  const after = text.slice(span.endIndex);
75
163
  // Collapse double blank lines created by removing the block, but keep the
76
- // user's own content. A single trailing newline at EOF is restored.
77
- let next = (before + after).replace(/\n{3,}/g, '\n\n');
78
- if (next.length > 0 && !next.endsWith('\n'))
79
- next += '\n';
80
- writeFileSync(filePath, next, 'utf8');
164
+ // user's own content. A single trailing newline at EOF is restored,
165
+ // matching whatever EOL the remaining content uses.
166
+ const remaining = before + after;
167
+ const eol = detectEol(remaining);
168
+ const collapsePattern = eol === '\r\n' ? /(?:\r\n){3,}/g : /\n{3,}/g;
169
+ const doubleEol = `${eol}${eol}`;
170
+ let next = remaining.replace(collapsePattern, doubleEol);
171
+ if (next.length > 0 && !next.endsWith(eol))
172
+ next += eol;
173
+ atomicWrite(filePath, next);
81
174
  return `Removed the browser-link instructions block from ${filePath} (${displayName}).`;
82
175
  }
83
176
  //# sourceMappingURL=file-ops.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-ops.js","sourceRoot":"","sources":["../../src/agent-instructions/file-ops.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"file-ops.js","sourceRoot":"","sources":["../../src/agent-instructions/file-ops.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,31 @@
1
1
  import type { ClientId, InstructionsInstaller } from './types.js';
2
+ /**
3
+ * Claude Code reads `~/.claude/CLAUDE.md` as the user-level global
4
+ * instructions file (applied to every project). That is where the block
5
+ * lives so the trigger list reaches every Claude session, regardless of
6
+ * which repo the user is in.
7
+ */
8
+ export declare const claudeInstructionsInstaller: InstructionsInstaller;
9
+ /**
10
+ * OpenCode follows the AGENTS.md convention (https://agents.md). Global
11
+ * instructions live next to the MCP config at
12
+ * `~/.config/opencode/AGENTS.md` on every OS — same dir as `opencode.json`.
13
+ * Project-level AGENTS.md still applies in addition; the global file is
14
+ * the one we manage so the agent gets the trigger list everywhere.
15
+ */
16
+ export declare const opencodeInstructionsInstaller: InstructionsInstaller;
17
+ /**
18
+ * GitHub Copilot CLI keeps its config under `~/.copilot/` (overridable via
19
+ * COPILOT_HOME, same env var the rest of our Copilot integration honours).
20
+ * AGENTS.md alongside `mcp-config.json` is the convention recent versions
21
+ * pick up; older releases ignore the file, in which case the block sits
22
+ * there harmlessly until they catch up.
23
+ *
24
+ * When `COPILOT_HOME` is set the resolved path may sit outside `$HOME`.
25
+ * That is the user opting in explicitly, so the outside-`$HOME` guard is
26
+ * relaxed for this client only when the env var is present.
27
+ */
28
+ export declare const copilotInstructionsInstaller: InstructionsInstaller;
2
29
  /**
3
30
  * Order matches the MCP `INSTALLERS` array so the agent-instructions screen
4
31
  * and the MCP installers screen list clients in the same sequence.
@@ -6,3 +33,4 @@ import type { ClientId, InstructionsInstaller } from './types.js';
6
33
  export declare const INSTRUCTIONS_INSTALLERS: InstructionsInstaller[];
7
34
  export declare function getInstructionsInstaller(id: ClientId): InstructionsInstaller;
8
35
  export type { ClientId, InstructionsDetect, InstructionsInstaller, InstructionsState, } from './types.js';
36
+ export { CorruptBlockError, OutsideHomeError, SymlinkRefusedError } from './errors.js';
@@ -1,6 +1,71 @@
1
- import { claudeInstructionsInstaller } from './claude.js';
2
- import { copilotInstructionsInstaller } from './copilot.js';
3
- import { opencodeInstructionsInstaller } from './opencode.js';
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { detectAt, installAt, uninstallAt } from './file-ops.js';
4
+ function createInstructionsInstaller(spec) {
5
+ const { id, displayName, file, allowOutsideHome } = spec;
6
+ const opts = () => ({
7
+ allowOutsideHome: allowOutsideHome?.() ?? false,
8
+ });
9
+ return {
10
+ id,
11
+ displayName,
12
+ filePath() {
13
+ return file();
14
+ },
15
+ detect() {
16
+ return detectAt(file());
17
+ },
18
+ install() {
19
+ return installAt(file(), displayName, opts());
20
+ },
21
+ uninstall() {
22
+ return uninstallAt(file(), displayName, opts());
23
+ },
24
+ };
25
+ }
26
+ /**
27
+ * Claude Code reads `~/.claude/CLAUDE.md` as the user-level global
28
+ * instructions file (applied to every project). That is where the block
29
+ * lives so the trigger list reaches every Claude session, regardless of
30
+ * which repo the user is in.
31
+ */
32
+ export const claudeInstructionsInstaller = createInstructionsInstaller({
33
+ id: 'claude',
34
+ displayName: 'Claude Code',
35
+ file: () => join(homedir(), '.claude', 'CLAUDE.md'),
36
+ });
37
+ /**
38
+ * OpenCode follows the AGENTS.md convention (https://agents.md). Global
39
+ * instructions live next to the MCP config at
40
+ * `~/.config/opencode/AGENTS.md` on every OS — same dir as `opencode.json`.
41
+ * Project-level AGENTS.md still applies in addition; the global file is
42
+ * the one we manage so the agent gets the trigger list everywhere.
43
+ */
44
+ export const opencodeInstructionsInstaller = createInstructionsInstaller({
45
+ id: 'opencode',
46
+ displayName: 'OpenCode',
47
+ file: () => join(homedir(), '.config', 'opencode', 'AGENTS.md'),
48
+ });
49
+ /**
50
+ * GitHub Copilot CLI keeps its config under `~/.copilot/` (overridable via
51
+ * COPILOT_HOME, same env var the rest of our Copilot integration honours).
52
+ * AGENTS.md alongside `mcp-config.json` is the convention recent versions
53
+ * pick up; older releases ignore the file, in which case the block sits
54
+ * there harmlessly until they catch up.
55
+ *
56
+ * When `COPILOT_HOME` is set the resolved path may sit outside `$HOME`.
57
+ * That is the user opting in explicitly, so the outside-`$HOME` guard is
58
+ * relaxed for this client only when the env var is present.
59
+ */
60
+ export const copilotInstructionsInstaller = createInstructionsInstaller({
61
+ id: 'copilot',
62
+ displayName: 'GitHub Copilot CLI',
63
+ file: () => {
64
+ const root = process.env.COPILOT_HOME ?? join(homedir(), '.copilot');
65
+ return join(root, 'AGENTS.md');
66
+ },
67
+ allowOutsideHome: () => process.env.COPILOT_HOME !== undefined,
68
+ });
4
69
  /**
5
70
  * Order matches the MCP `INSTALLERS` array so the agent-instructions screen
6
71
  * and the MCP installers screen list clients in the same sequence.
@@ -16,4 +81,5 @@ export function getInstructionsInstaller(id) {
16
81
  throw new Error(`Unknown or unsupported client: ${id}`);
17
82
  return found;
18
83
  }
84
+ export { CorruptBlockError, OutsideHomeError, SymlinkRefusedError } from './errors.js';
19
85
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/agent-instructions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,4BAA4B,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,6BAA6B,EAAE,MAAM,eAAe,CAAC;AAG9D;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAA4B;IAC9D,2BAA2B;IAC3B,6BAA6B;IAC7B,4BAA4B;CAC7B,CAAC;AAEF,MAAM,UAAU,wBAAwB,CAAC,EAAY;IACnD,MAAM,KAAK,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,EAAE,EAAE,CAAC,CAAC;IACpE,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/agent-instructions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAwBjE,SAAS,2BAA2B,CAAC,IAAmB;IACtD,MAAM,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,EAAE,GAAG,IAAI,CAAC;IACzD,MAAM,IAAI,GAAG,GAAkC,EAAE,CAAC,CAAC;QACjD,gBAAgB,EAAE,gBAAgB,EAAE,EAAE,IAAI,KAAK;KAChD,CAAC,CAAC;IACH,OAAO;QACL,EAAE;QACF,WAAW;QACX,QAAQ;YACN,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QACD,MAAM;YACJ,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1B,CAAC;QACD,OAAO;YACL,OAAO,SAAS,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,CAAC;QACD,SAAS;YACP,OAAO,WAAW,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAA0B,2BAA2B,CAAC;IAC5F,EAAE,EAAE,QAAQ;IACZ,WAAW,EAAE,aAAa;IAC1B,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,WAAW,CAAC;CACpD,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAA0B,2BAA2B,CAAC;IAC9F,EAAE,EAAE,UAAU;IACd,WAAW,EAAE,UAAU;IACvB,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,CAAC;CAChE,CAAC,CAAC;AAEH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAA0B,2BAA2B,CAAC;IAC7F,EAAE,EAAE,SAAS;IACb,WAAW,EAAE,oBAAoB;IACjC,IAAI,EAAE,GAAG,EAAE;QACT,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;QACrE,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACjC,CAAC;IACD,gBAAgB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,SAAS;CAC/D,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAA4B;IAC9D,2BAA2B;IAC3B,6BAA6B;IAC7B,4BAA4B;CAC7B,CAAC;AAEF,MAAM,UAAU,wBAAwB,CAAC,EAAY;IACnD,MAAM,KAAK,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,EAAE,EAAE,CAAC,CAAC;IACpE,OAAO,KAAK,CAAC;AACf,CAAC;AASD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
@@ -8,10 +8,13 @@ export type InstructionsState = {
8
8
  kind: 'not-installed';
9
9
  } | {
10
10
  kind: 'installed';
11
- version: string;
11
+ version: string | null;
12
12
  } | {
13
13
  kind: 'installed-outdated';
14
- version: string;
14
+ version: string | null;
15
+ } | {
16
+ kind: 'corrupt';
17
+ reason: 'multiple-begin-markers';
15
18
  };
16
19
  export interface InstructionsDetect {
17
20
  /** Absolute path to the markdown file we manage. Returned even when it does not exist. */