@bastani/atomic 0.6.5 → 0.6.6-1
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/.agents/skills/ado-commit/SKILL.md +2 -0
- package/.agents/skills/ado-create-pr/SKILL.md +2 -0
- package/.agents/skills/advanced-evaluation/SKILL.md +2 -0
- package/.agents/skills/ast-grep/SKILL.md +2 -0
- package/.agents/skills/bdi-mental-states/SKILL.md +2 -0
- package/.agents/skills/bun/SKILL.md +156 -122
- package/.agents/skills/context-compression/SKILL.md +2 -0
- package/.agents/skills/context-degradation/SKILL.md +2 -0
- package/.agents/skills/context-fundamentals/SKILL.md +2 -0
- package/.agents/skills/context-optimization/SKILL.md +2 -0
- package/.agents/skills/create-spec/SKILL.md +2 -0
- package/.agents/skills/docx/SKILL.md +2 -0
- package/.agents/skills/evaluation/SKILL.md +2 -0
- package/.agents/skills/explain-code/SKILL.md +2 -0
- package/.agents/skills/filesystem-context/SKILL.md +2 -0
- package/.agents/skills/find-skills/SKILL.md +2 -0
- package/.agents/skills/gh-commit/SKILL.md +2 -0
- package/.agents/skills/gh-create-pr/SKILL.md +2 -0
- package/.agents/skills/hosted-agents/SKILL.md +2 -0
- package/.agents/skills/impeccable/SKILL.md +117 -304
- package/.agents/skills/impeccable/agents/openai.yaml +4 -0
- package/.agents/skills/{adapt/SKILL.md → impeccable/reference/adapt.md} +2 -11
- package/.agents/skills/{animate/SKILL.md → impeccable/reference/animate.md} +15 -15
- package/.agents/skills/{audit/SKILL.md → impeccable/reference/audit.md} +8 -22
- package/.agents/skills/{bolder/SKILL.md → impeccable/reference/bolder.md} +9 -13
- package/.agents/skills/impeccable/reference/brand.md +114 -0
- package/.agents/skills/{clarify/SKILL.md → impeccable/reference/clarify.md} +2 -11
- package/.agents/skills/{colorize/SKILL.md → impeccable/reference/colorize.md} +23 -12
- package/.agents/skills/impeccable/reference/craft.md +152 -29
- package/.agents/skills/{critique/SKILL.md → impeccable/reference/critique.md} +25 -37
- package/.agents/skills/{delight/SKILL.md → impeccable/reference/delight.md} +9 -11
- package/.agents/skills/{distill/SKILL.md → impeccable/reference/distill.md} +2 -13
- package/.agents/skills/impeccable/reference/document.md +427 -0
- package/.agents/skills/impeccable/reference/extract.md +1 -1
- package/.agents/skills/{harden/SKILL.md → impeccable/reference/harden.md} +1 -43
- package/.agents/skills/{layout/SKILL.md → impeccable/reference/layout.md} +27 -11
- package/.agents/skills/impeccable/reference/live.md +594 -0
- package/.agents/skills/impeccable/reference/motion-design.md +12 -2
- package/.agents/skills/impeccable/reference/onboard.md +234 -0
- package/.agents/skills/{optimize/SKILL.md → impeccable/reference/optimize.md} +4 -12
- package/.agents/skills/{overdrive/SKILL.md → impeccable/reference/overdrive.md} +9 -21
- package/.agents/skills/{critique → impeccable}/reference/personas.md +1 -1
- package/.agents/skills/{polish/SKILL.md → impeccable/reference/polish.md} +31 -23
- package/.agents/skills/impeccable/reference/product.md +62 -0
- package/.agents/skills/{quieter/SKILL.md → impeccable/reference/quieter.md} +7 -11
- package/.agents/skills/impeccable/reference/shape.md +151 -0
- package/.agents/skills/impeccable/reference/teach.md +156 -0
- package/.agents/skills/{typeset/SKILL.md → impeccable/reference/typeset.md} +19 -11
- package/.agents/skills/impeccable/reference/typography.md +31 -14
- package/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs +87 -17
- package/.agents/skills/impeccable/scripts/command-metadata.json +94 -0
- package/.agents/skills/impeccable/scripts/design-parser.mjs +820 -0
- package/.agents/skills/impeccable/scripts/detect-csp.mjs +198 -0
- package/.agents/skills/impeccable/scripts/is-generated.mjs +69 -0
- package/.agents/skills/impeccable/scripts/live-accept.mjs +595 -0
- package/.agents/skills/impeccable/scripts/live-browser.js +4781 -0
- package/.agents/skills/impeccable/scripts/live-inject.mjs +445 -0
- package/.agents/skills/impeccable/scripts/live-poll.mjs +186 -0
- package/.agents/skills/impeccable/scripts/live-server.mjs +694 -0
- package/.agents/skills/impeccable/scripts/live-wrap.mjs +571 -0
- package/.agents/skills/impeccable/scripts/live.mjs +247 -0
- package/.agents/skills/impeccable/scripts/load-context.mjs +141 -0
- package/.agents/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/.agents/skills/impeccable/scripts/pin.mjs +214 -0
- package/.agents/skills/init/SKILL.md +2 -0
- package/.agents/skills/liteparse/SKILL.md +1 -0
- package/.agents/skills/memory-systems/SKILL.md +2 -0
- package/.agents/skills/multi-agent-patterns/SKILL.md +2 -0
- package/.agents/skills/opentui/SKILL.md +1 -0
- package/.agents/skills/pdf/SKILL.md +2 -0
- package/.agents/skills/playwright-cli/SKILL.md +51 -5
- package/.agents/skills/playwright-cli/references/playwright-tests.md +1 -1
- package/.agents/skills/playwright-cli/references/running-code.md +10 -0
- package/.agents/skills/playwright-cli/references/session-management.md +56 -0
- package/.agents/skills/playwright-cli/references/spec-driven-testing.md +305 -0
- package/.agents/skills/playwright-cli/references/test-generation.md +49 -3
- package/.agents/skills/pptx/SKILL.md +2 -0
- package/.agents/skills/project-development/SKILL.md +2 -0
- package/.agents/skills/prompt-engineer/SKILL.md +2 -0
- package/.agents/skills/research-codebase/SKILL.md +2 -0
- package/.agents/skills/ripgrep/SKILL.md +2 -0
- package/.agents/skills/skill-creator/LICENSE.txt +1 -1
- package/.agents/skills/skill-creator/SKILL.md +2 -0
- package/.agents/skills/sl-commit/SKILL.md +2 -0
- package/.agents/skills/sl-submit-diff/SKILL.md +2 -0
- package/.agents/skills/tdd/SKILL.md +4 -0
- package/.agents/skills/tool-design/SKILL.md +2 -0
- package/.agents/skills/typescript-advanced-types/SKILL.md +2 -1
- package/.agents/skills/typescript-expert/SKILL.md +7 -1
- package/.agents/skills/typescript-react-reviewer/SKILL.md +2 -1
- package/.agents/skills/workflow-creator/SKILL.md +75 -72
- package/.agents/skills/workflow-creator/references/session-config.md +48 -1
- package/.agents/skills/xlsx/SKILL.md +2 -0
- package/.opencode/opencode.json +6 -2
- package/README.md +39 -38
- package/dist/lib/atomic-temp.d.ts +8 -0
- package/dist/lib/atomic-temp.d.ts.map +1 -0
- package/dist/lib/terminal-env.d.ts +9 -0
- package/dist/lib/terminal-env.d.ts.map +1 -0
- package/dist/sdk/providers/claude.d.ts.map +1 -1
- package/dist/sdk/providers/copilot.d.ts +24 -14
- package/dist/sdk/providers/copilot.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts +8 -0
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/port-discovery.d.ts +71 -0
- package/dist/sdk/runtime/port-discovery.d.ts.map +1 -0
- package/dist/sdk/runtime/tmux.d.ts +10 -0
- package/dist/sdk/runtime/tmux.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +1 -0
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +15 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
- package/package.json +10 -10
- package/src/commands/cli/chat/index.test.ts +194 -2
- package/src/commands/cli/chat/index.ts +83 -28
- package/src/lib/atomic-temp.test.ts +86 -0
- package/src/lib/atomic-temp.ts +62 -0
- package/src/lib/terminal-env.test.ts +343 -0
- package/src/lib/terminal-env.ts +100 -0
- package/src/scripts/clean-dist.test.ts +53 -0
- package/src/scripts/clean-dist.ts +37 -0
- package/src/sdk/providers/claude.ts +42 -20
- package/src/sdk/providers/copilot.test.ts +365 -0
- package/src/sdk/providers/copilot.ts +117 -15
- package/src/sdk/runtime/cc-debounce.ts +2 -2
- package/src/sdk/runtime/executor.test.ts +322 -1
- package/src/sdk/runtime/executor.ts +159 -96
- package/src/sdk/runtime/port-discovery.test.ts +573 -0
- package/src/sdk/runtime/port-discovery.ts +496 -0
- package/src/sdk/runtime/tmux.ts +22 -2
- package/src/sdk/types.ts +1 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +24 -6
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +52 -13
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +31 -3
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +16 -0
- package/src/sdk/workflows/builtin/ralph/helpers/prompts.ts +70 -3
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +50 -6
- package/src/services/system/auth.test.ts +53 -0
- package/src/services/system/auth.ts +31 -28
- package/src/services/system/detect.ts +1 -1
- package/.agents/skills/shape/SKILL.md +0 -96
- /package/.agents/skills/{critique → impeccable}/reference/cognitive-load.md +0 -0
- /package/.agents/skills/{critique → impeccable}/reference/heuristics-scoring.md +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import {
|
|
6
|
+
copilotSubprocessEnv,
|
|
7
|
+
copilotSdkLaunchOptions,
|
|
8
|
+
enumeratePathCandidates,
|
|
9
|
+
isCopilotShim,
|
|
10
|
+
resolveCopilotCliPath,
|
|
11
|
+
} from "./copilot.ts";
|
|
12
|
+
|
|
13
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
function makeTempCopilotBin(): { dir: string; bin: string } {
|
|
16
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-copilot-test-"));
|
|
17
|
+
const bin = join(dir, "copilot");
|
|
18
|
+
writeFileSync(bin, "#!/bin/sh\necho copilot\n", { encoding: "utf-8" });
|
|
19
|
+
chmodSync(bin, 0o755);
|
|
20
|
+
return { dir, bin };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeEmptyTempDir(): string {
|
|
24
|
+
return mkdtempSync(join(tmpdir(), "atomic-copilot-empty-"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Creates a dir with a `copilot` file that has a node shebang (shim). */
|
|
28
|
+
function makeNodeShimDir(): string {
|
|
29
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-copilot-shim-"));
|
|
30
|
+
const bin = join(dir, "copilot");
|
|
31
|
+
writeFileSync(bin, "#!/usr/bin/env node\nconsole.log('shim');\n", { encoding: "utf-8" });
|
|
32
|
+
chmodSync(bin, 0o755);
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Creates a dir with a `copilot` file that has a .js extension. */
|
|
37
|
+
function makeJsExtensionDir(): string {
|
|
38
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-copilot-jsext-"));
|
|
39
|
+
const bin = join(dir, "copilot.js");
|
|
40
|
+
writeFileSync(bin, "#!/usr/bin/env node\n", { encoding: "utf-8" });
|
|
41
|
+
chmodSync(bin, 0o755);
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Creates a node_modules/.bin/copilot symlink pointing to a .js loader file. */
|
|
46
|
+
function makeNpmLoaderShimDir(): { dir: string; bin: string; target: string } {
|
|
47
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-copilot-npmloader-"));
|
|
48
|
+
const binDir = join(dir, "node_modules", ".bin");
|
|
49
|
+
mkdirSync(binDir, { recursive: true });
|
|
50
|
+
const target = join(dir, "npm-loader.js");
|
|
51
|
+
writeFileSync(target, "// npm-loader.js shim\n", { encoding: "utf-8" });
|
|
52
|
+
const bin = join(binDir, "copilot");
|
|
53
|
+
symlinkSync(target, bin);
|
|
54
|
+
return { dir, bin, target };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Creates a dir with a `copilot` file containing npm-loader.js marker in header. */
|
|
58
|
+
function makeNpmLoaderMarkerDir(): string {
|
|
59
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-copilot-npmmarker-"));
|
|
60
|
+
const bin = join(dir, "copilot");
|
|
61
|
+
writeFileSync(bin, "#!/bin/sh\n# loads npm-loader.js\n", { encoding: "utf-8" });
|
|
62
|
+
chmodSync(bin, 0o755);
|
|
63
|
+
return dir;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── resolveCopilotCliPath ─────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
describe("resolveCopilotCliPath", () => {
|
|
69
|
+
let origCliPath: string | undefined;
|
|
70
|
+
let origPath: string | undefined;
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
origCliPath = process.env["COPILOT_CLI_PATH"];
|
|
74
|
+
origPath = process.env["PATH"];
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
if (origCliPath === undefined) {
|
|
79
|
+
delete process.env["COPILOT_CLI_PATH"];
|
|
80
|
+
} else {
|
|
81
|
+
process.env["COPILOT_CLI_PATH"] = origCliPath;
|
|
82
|
+
}
|
|
83
|
+
if (origPath === undefined) {
|
|
84
|
+
delete process.env["PATH"];
|
|
85
|
+
} else {
|
|
86
|
+
process.env["PATH"] = origPath;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("COPILOT_CLI_PATH env var takes precedence over PATH", () => {
|
|
91
|
+
const explicit = "/custom/bin/copilot";
|
|
92
|
+
process.env["COPILOT_CLI_PATH"] = explicit;
|
|
93
|
+
// even if PATH has a copilot binary, the env var wins
|
|
94
|
+
expect(resolveCopilotCliPath()).toBe(explicit);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("PATH-resolved copilot binary populates cliPath when env var unset", () => {
|
|
98
|
+
delete process.env["COPILOT_CLI_PATH"];
|
|
99
|
+
const { dir, bin } = makeTempCopilotBin();
|
|
100
|
+
process.env["PATH"] = `${dir}:${origPath ?? ""}`;
|
|
101
|
+
const result = resolveCopilotCliPath();
|
|
102
|
+
expect(result).toBe(bin);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("returns undefined when copilot not on PATH and env var unset", () => {
|
|
106
|
+
delete process.env["COPILOT_CLI_PATH"];
|
|
107
|
+
const emptyDir = makeEmptyTempDir();
|
|
108
|
+
process.env["PATH"] = emptyDir;
|
|
109
|
+
const result = resolveCopilotCliPath();
|
|
110
|
+
expect(result).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ── copilotSubprocessEnv ──────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
describe("copilotSubprocessEnv", () => {
|
|
117
|
+
test("NODE_NO_WARNINGS is set to '1'", () => {
|
|
118
|
+
const env = copilotSubprocessEnv({});
|
|
119
|
+
expect(env["NODE_NO_WARNINGS"]).toBe("1");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("UTF-8 locale defaults applied when base env empty", () => {
|
|
123
|
+
const env = copilotSubprocessEnv({});
|
|
124
|
+
expect(env["LANG"]).toBe("en_US.UTF-8");
|
|
125
|
+
expect(env["LC_ALL"]).toBe("en_US.UTF-8");
|
|
126
|
+
expect(env["LC_CTYPE"]).toBe("en_US.UTF-8");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("UTF-8 env vars from base merged into result", () => {
|
|
130
|
+
const base = {
|
|
131
|
+
LANG: "fr_FR.UTF-8",
|
|
132
|
+
LC_ALL: "fr_FR.UTF-8",
|
|
133
|
+
LC_CTYPE: "fr_FR.UTF-8",
|
|
134
|
+
MY_CUSTOM: "hello",
|
|
135
|
+
};
|
|
136
|
+
const env = copilotSubprocessEnv(base);
|
|
137
|
+
expect(env["LANG"]).toBe("fr_FR.UTF-8");
|
|
138
|
+
expect(env["LC_ALL"]).toBe("fr_FR.UTF-8");
|
|
139
|
+
expect(env["MY_CUSTOM"]).toBe("hello");
|
|
140
|
+
expect(env["NODE_NO_WARNINGS"]).toBe("1");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("NODE_NO_WARNINGS=1 overrides any base value", () => {
|
|
144
|
+
const env = copilotSubprocessEnv({ NODE_NO_WARNINGS: "0" });
|
|
145
|
+
expect(env["NODE_NO_WARNINGS"]).toBe("1");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("returns fresh object per call (no shared state)", () => {
|
|
149
|
+
const a = copilotSubprocessEnv({});
|
|
150
|
+
const b = copilotSubprocessEnv({});
|
|
151
|
+
expect(a).not.toBe(b);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ── copilotSdkLaunchOptions ───────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
describe("copilotSdkLaunchOptions", () => {
|
|
158
|
+
let origCliPath: string | undefined;
|
|
159
|
+
let origPath: string | undefined;
|
|
160
|
+
|
|
161
|
+
beforeEach(() => {
|
|
162
|
+
origCliPath = process.env["COPILOT_CLI_PATH"];
|
|
163
|
+
origPath = process.env["PATH"];
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
afterEach(() => {
|
|
167
|
+
if (origCliPath === undefined) {
|
|
168
|
+
delete process.env["COPILOT_CLI_PATH"];
|
|
169
|
+
} else {
|
|
170
|
+
process.env["COPILOT_CLI_PATH"] = origCliPath;
|
|
171
|
+
}
|
|
172
|
+
if (origPath === undefined) {
|
|
173
|
+
delete process.env["PATH"];
|
|
174
|
+
} else {
|
|
175
|
+
process.env["PATH"] = origPath;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("env contains NODE_NO_WARNINGS=1", () => {
|
|
180
|
+
const opts = copilotSdkLaunchOptions();
|
|
181
|
+
expect(opts.env?.["NODE_NO_WARNINGS"]).toBe("1");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("cliPath populated from COPILOT_CLI_PATH", () => {
|
|
185
|
+
process.env["COPILOT_CLI_PATH"] = "/my/copilot";
|
|
186
|
+
const opts = copilotSdkLaunchOptions();
|
|
187
|
+
expect(opts.cliPath).toBe("/my/copilot");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("cliPath omitted when copilot not resolvable", () => {
|
|
191
|
+
delete process.env["COPILOT_CLI_PATH"];
|
|
192
|
+
const emptyDir = makeEmptyTempDir();
|
|
193
|
+
process.env["PATH"] = emptyDir;
|
|
194
|
+
const opts = copilotSdkLaunchOptions();
|
|
195
|
+
expect("cliPath" in opts).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("cliPath populated from PATH-resolved binary", () => {
|
|
199
|
+
delete process.env["COPILOT_CLI_PATH"];
|
|
200
|
+
const { dir, bin } = makeTempCopilotBin();
|
|
201
|
+
process.env["PATH"] = `${dir}:${origPath ?? ""}`;
|
|
202
|
+
const opts = copilotSdkLaunchOptions();
|
|
203
|
+
expect(opts.cliPath).toBe(bin);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("cliPath omitted when only shim on PATH", () => {
|
|
207
|
+
delete process.env["COPILOT_CLI_PATH"];
|
|
208
|
+
const shimDir = makeNodeShimDir();
|
|
209
|
+
process.env["PATH"] = shimDir;
|
|
210
|
+
const opts = copilotSdkLaunchOptions();
|
|
211
|
+
expect("cliPath" in opts).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ── isCopilotShim ─────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
describe("isCopilotShim", () => {
|
|
218
|
+
test("returns true for .js extension", () => {
|
|
219
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-shim-jsext-"));
|
|
220
|
+
const p = join(dir, "copilot.js");
|
|
221
|
+
writeFileSync(p, "#!/usr/bin/env node\n", { encoding: "utf-8" });
|
|
222
|
+
expect(isCopilotShim(p)).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("returns true for .mjs extension", () => {
|
|
226
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-shim-mjs-"));
|
|
227
|
+
const p = join(dir, "copilot.mjs");
|
|
228
|
+
writeFileSync(p, "export default {}\n", { encoding: "utf-8" });
|
|
229
|
+
expect(isCopilotShim(p)).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("returns true for .cjs extension", () => {
|
|
233
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-shim-cjs-"));
|
|
234
|
+
const p = join(dir, "copilot.cjs");
|
|
235
|
+
writeFileSync(p, "module.exports = {}\n", { encoding: "utf-8" });
|
|
236
|
+
expect(isCopilotShim(p)).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("returns true for #!/usr/bin/env node shebang", () => {
|
|
240
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-shim-shebang-"));
|
|
241
|
+
const p = join(dir, "copilot");
|
|
242
|
+
writeFileSync(p, "#!/usr/bin/env node\nconsole.log('hi');\n", { encoding: "utf-8" });
|
|
243
|
+
chmodSync(p, 0o755);
|
|
244
|
+
expect(isCopilotShim(p)).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("returns true for #!/usr/bin/node shebang", () => {
|
|
248
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-shim-shebang2-"));
|
|
249
|
+
const p = join(dir, "copilot");
|
|
250
|
+
writeFileSync(p, "#!/usr/bin/node\nconsole.log('hi');\n", { encoding: "utf-8" });
|
|
251
|
+
chmodSync(p, 0o755);
|
|
252
|
+
expect(isCopilotShim(p)).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("returns true when header contains npm-loader.js marker", () => {
|
|
256
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-shim-marker-"));
|
|
257
|
+
const p = join(dir, "copilot");
|
|
258
|
+
writeFileSync(p, "#!/bin/sh\n# loads npm-loader.js internally\n", { encoding: "utf-8" });
|
|
259
|
+
chmodSync(p, 0o755);
|
|
260
|
+
expect(isCopilotShim(p)).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("returns true for node_modules/.bin/copilot symlink pointing to .js file", () => {
|
|
264
|
+
const { bin } = makeNpmLoaderShimDir();
|
|
265
|
+
expect(isCopilotShim(bin)).toBe(true);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("returns false for plain shell script binary", () => {
|
|
269
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-shim-shell-"));
|
|
270
|
+
const p = join(dir, "copilot");
|
|
271
|
+
writeFileSync(p, "#!/bin/sh\necho copilot\n", { encoding: "utf-8" });
|
|
272
|
+
chmodSync(p, 0o755);
|
|
273
|
+
expect(isCopilotShim(p)).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("returns false for missing file (let SDK surface the error)", () => {
|
|
277
|
+
expect(isCopilotShim("/nonexistent/path/copilot")).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ── enumeratePathCandidates ───────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
describe("enumeratePathCandidates", () => {
|
|
284
|
+
test("returns empty array when PATH has no matching binary", () => {
|
|
285
|
+
const empty = makeEmptyTempDir();
|
|
286
|
+
expect(enumeratePathCandidates("copilot", empty)).toEqual([]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("returns single match when one dir has binary", () => {
|
|
290
|
+
const { dir, bin } = makeTempCopilotBin();
|
|
291
|
+
const result = enumeratePathCandidates("copilot", dir);
|
|
292
|
+
expect(result).toEqual([bin]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("returns ordered matches from multiple PATH dirs", () => {
|
|
296
|
+
const { dir: dir1, bin: bin1 } = makeTempCopilotBin();
|
|
297
|
+
const { dir: dir2, bin: bin2 } = makeTempCopilotBin();
|
|
298
|
+
const result = enumeratePathCandidates("copilot", `${dir1}:${dir2}`);
|
|
299
|
+
expect(result).toEqual([bin1, bin2]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("skips dirs that do not contain the binary", () => {
|
|
303
|
+
const empty = makeEmptyTempDir();
|
|
304
|
+
const { dir, bin } = makeTempCopilotBin();
|
|
305
|
+
const result = enumeratePathCandidates("copilot", `${empty}:${dir}`);
|
|
306
|
+
expect(result).toEqual([bin]);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ── resolveCopilotCliPath shim rejection ──────────────────────────────────
|
|
311
|
+
|
|
312
|
+
describe("resolveCopilotCliPath shim rejection", () => {
|
|
313
|
+
let origCliPath: string | undefined;
|
|
314
|
+
let origPath: string | undefined;
|
|
315
|
+
|
|
316
|
+
beforeEach(() => {
|
|
317
|
+
origCliPath = process.env["COPILOT_CLI_PATH"];
|
|
318
|
+
origPath = process.env["PATH"];
|
|
319
|
+
delete process.env["COPILOT_CLI_PATH"];
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
afterEach(() => {
|
|
323
|
+
if (origCliPath === undefined) {
|
|
324
|
+
delete process.env["COPILOT_CLI_PATH"];
|
|
325
|
+
} else {
|
|
326
|
+
process.env["COPILOT_CLI_PATH"] = origCliPath;
|
|
327
|
+
}
|
|
328
|
+
if (origPath === undefined) {
|
|
329
|
+
delete process.env["PATH"];
|
|
330
|
+
} else {
|
|
331
|
+
process.env["PATH"] = origPath;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("returns undefined when only node-shebang shim exists on PATH", () => {
|
|
336
|
+
process.env["PATH"] = makeNodeShimDir();
|
|
337
|
+
expect(resolveCopilotCliPath()).toBeUndefined();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("returns undefined when only npm-loader.js symlink shim on PATH", () => {
|
|
341
|
+
const { dir } = makeNpmLoaderShimDir();
|
|
342
|
+
const binDir = join(dir, "node_modules", ".bin");
|
|
343
|
+
process.env["PATH"] = binDir;
|
|
344
|
+
expect(resolveCopilotCliPath()).toBeUndefined();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("returns undefined when only npm-loader.js marker shim on PATH", () => {
|
|
348
|
+
process.env["PATH"] = makeNpmLoaderMarkerDir();
|
|
349
|
+
expect(resolveCopilotCliPath()).toBeUndefined();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("skips shim and returns second PATH candidate (real binary)", () => {
|
|
353
|
+
const shimDir = makeNodeShimDir();
|
|
354
|
+
const { dir: realDir, bin: realBin } = makeTempCopilotBin();
|
|
355
|
+
process.env["PATH"] = `${shimDir}:${realDir}`;
|
|
356
|
+
expect(resolveCopilotCliPath()).toBe(realBin);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("returns first non-shim when multiple real binaries on PATH", () => {
|
|
360
|
+
const { dir: dir1, bin: bin1 } = makeTempCopilotBin();
|
|
361
|
+
const { dir: dir2 } = makeTempCopilotBin();
|
|
362
|
+
process.env["PATH"] = `${dir1}:${dir2}`;
|
|
363
|
+
expect(resolveCopilotCliPath()).toBe(bin1);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
@@ -5,27 +5,129 @@
|
|
|
5
5
|
* `s.client` and `s.session` instead of manual SDK client creation.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import { closeSync, existsSync, openSync, readSync, realpathSync } from "node:fs";
|
|
9
|
+
import { delimiter, join, sep } from "node:path";
|
|
10
|
+
import type {
|
|
11
|
+
CopilotClientOptions,
|
|
12
|
+
SessionConfig as CopilotSessionConfig,
|
|
13
|
+
} from "@github/copilot-sdk";
|
|
14
|
+
import { normalizedTerminalEnv } from "../../lib/terminal-env.ts";
|
|
15
|
+
import { getCommandPath } from "../../services/system/detect.ts";
|
|
9
16
|
import { createProviderValidator } from "../types.ts";
|
|
10
17
|
|
|
18
|
+
const JS_EXT_RE = /\.(js|mjs|cjs)$/i;
|
|
19
|
+
const NODE_SHEBANG_RE = /^#!.*\bnode\b/;
|
|
20
|
+
const NPM_LOADER_MARKER = "npm-loader.js";
|
|
21
|
+
const HEADER_BYTES = 256;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read the first {@link HEADER_BYTES} of a file as a UTF-8 string.
|
|
25
|
+
* Returns `null` on any filesystem error (file missing, not readable, etc.).
|
|
26
|
+
*/
|
|
27
|
+
function readCandidateHeader(filePath: string): string | null {
|
|
28
|
+
let fd: number | undefined;
|
|
29
|
+
try {
|
|
30
|
+
fd = openSync(filePath, "r");
|
|
31
|
+
const buffer = Buffer.alloc(HEADER_BYTES);
|
|
32
|
+
const bytesRead = readSync(fd, buffer, 0, HEADER_BYTES, 0);
|
|
33
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
} finally {
|
|
37
|
+
if (fd !== undefined) closeSync(fd);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve symlink chain to final target path.
|
|
43
|
+
* Returns the original path on any filesystem error.
|
|
44
|
+
*/
|
|
45
|
+
function safeRealpath(filePath: string): string {
|
|
46
|
+
try {
|
|
47
|
+
return realpathSync(filePath);
|
|
48
|
+
} catch {
|
|
49
|
+
return filePath;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
11
53
|
/**
|
|
12
|
-
*
|
|
54
|
+
* Return `true` when `candidate` is a Node.js / npm-loader JavaScript shim
|
|
55
|
+
* that should not be passed to the Copilot SDK as the CLI executable.
|
|
13
56
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
57
|
+
* Filesystem errors for a given candidate are treated as "not a shim" so
|
|
58
|
+
* that the SDK can surface the real error (e.g. permission denied, ENOENT).
|
|
59
|
+
*/
|
|
60
|
+
export function isCopilotShim(candidate: string): boolean {
|
|
61
|
+
if (JS_EXT_RE.test(candidate)) return true;
|
|
62
|
+
|
|
63
|
+
if (candidate.includes(`node_modules${sep}.bin`) || candidate.includes("node_modules/.bin")) {
|
|
64
|
+
const real = safeRealpath(candidate);
|
|
65
|
+
if (JS_EXT_RE.test(real)) return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const header = readCandidateHeader(candidate);
|
|
69
|
+
if (header === null) return false;
|
|
70
|
+
|
|
71
|
+
return NODE_SHEBANG_RE.test(header) || header.includes(NPM_LOADER_MARKER);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build the subprocess environment for the Copilot CLI process.
|
|
76
|
+
* Normalises locale to UTF-8 and suppresses Node.js deprecation warnings.
|
|
77
|
+
*/
|
|
78
|
+
export function copilotSubprocessEnv(
|
|
79
|
+
baseEnv: NodeJS.ProcessEnv = process.env,
|
|
80
|
+
): Record<string, string | undefined> {
|
|
81
|
+
return { ...normalizedTerminalEnv(baseEnv), NODE_NO_WARNINGS: "1" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Enumerate every existing `cmd` candidate across PATH order.
|
|
86
|
+
*/
|
|
87
|
+
export function enumeratePathCandidates(cmd: string, pathEnv: string): string[] {
|
|
88
|
+
const dirs = pathEnv.split(delimiter).filter(Boolean);
|
|
89
|
+
const results: string[] = [];
|
|
90
|
+
for (const dir of dirs) {
|
|
91
|
+
const full = join(dir, cmd);
|
|
92
|
+
if (existsSync(full)) results.push(full);
|
|
93
|
+
}
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function resolveCopilotCliPath(): string | undefined {
|
|
98
|
+
const envPath = process.env["COPILOT_CLI_PATH"];
|
|
99
|
+
if (envPath) return envPath;
|
|
100
|
+
|
|
101
|
+
const primary = getCommandPath("copilot");
|
|
102
|
+
if (primary === null) return undefined;
|
|
103
|
+
if (!isCopilotShim(primary)) return primary;
|
|
104
|
+
|
|
105
|
+
const pathEnv = process.env["PATH"] ?? "";
|
|
106
|
+
const candidates = enumeratePathCandidates("copilot", pathEnv);
|
|
107
|
+
for (const candidate of candidates) {
|
|
108
|
+
if (!isCopilotShim(candidate)) return candidate;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build options suitable for `new CopilotClient(...)`.
|
|
21
116
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
117
|
+
* Includes:
|
|
118
|
+
* - `env` from {@link copilotSubprocessEnv} (UTF-8 locale + `NODE_NO_WARNINGS=1`).
|
|
119
|
+
* - `cliPath` from {@link resolveCopilotCliPath} when resolvable; omitted
|
|
120
|
+
* otherwise so the SDK falls back to its bundled CLI.
|
|
26
121
|
*/
|
|
27
|
-
export function
|
|
28
|
-
|
|
122
|
+
export function copilotSdkLaunchOptions(): CopilotClientOptions {
|
|
123
|
+
const options: CopilotClientOptions = {
|
|
124
|
+
env: copilotSubprocessEnv(),
|
|
125
|
+
};
|
|
126
|
+
const cliPath = resolveCopilotCliPath();
|
|
127
|
+
if (cliPath !== undefined) {
|
|
128
|
+
options.cliPath = cliPath;
|
|
129
|
+
}
|
|
130
|
+
return options;
|
|
29
131
|
}
|
|
30
132
|
|
|
31
133
|
/**
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* confirmation.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { tmpdir } from "node:os";
|
|
22
21
|
import { join } from "node:path";
|
|
23
22
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { ensureAtomicTempDir } from "../../lib/atomic-temp.ts";
|
|
24
24
|
|
|
25
25
|
/** Quiet period (ms) the user must leave between presses for the next
|
|
26
26
|
* one to be forwarded. Must exceed every integrated agent's exit-confirm
|
|
@@ -44,7 +44,7 @@ export function shouldForward(
|
|
|
44
44
|
* something with shell metacharacters. */
|
|
45
45
|
function stateFileFor(paneId: string): string {
|
|
46
46
|
const safe = paneId.replace(/[^a-zA-Z0-9_%-]/g, "_");
|
|
47
|
-
return join(
|
|
47
|
+
return join(ensureAtomicTempDir(), `atomic-cc-${safe}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function readLastPress(stateFile: string): number {
|