@web42/cli 0.1.17 → 0.2.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.
@@ -0,0 +1,206 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import express from 'express';
7
+ import { agentCardHandler, jsonRpcHandler, } from '@a2a-js/sdk/server/express';
8
+ import { DefaultRequestHandler, InMemoryTaskStore, } from '@a2a-js/sdk/server';
9
+ import { requireAuth } from '../utils/config.js';
10
+ import { getConfig } from '../utils/config.js';
11
+ class OpenClawAgentExecutor {
12
+ opts;
13
+ constructor(opts) {
14
+ this.opts = opts;
15
+ }
16
+ async execute(requestContext, eventBus) {
17
+ const { taskId, contextId, userMessage } = requestContext;
18
+ const userText = userMessage.parts
19
+ .find((p) => p.kind === 'text')?.text ?? '';
20
+ let response;
21
+ try {
22
+ response = await fetch(`http://localhost:${this.opts.openClawPort}/v1/chat/completions`, {
23
+ method: 'POST',
24
+ headers: {
25
+ Authorization: `Bearer ${this.opts.openClawToken}`,
26
+ 'Content-Type': 'application/json',
27
+ 'x-openclaw-agent-id': this.opts.openClawAgent,
28
+ 'x-openclaw-session-key': contextId,
29
+ },
30
+ body: JSON.stringify({
31
+ model: 'openclaw',
32
+ stream: true,
33
+ messages: [{ role: 'user', content: userText }],
34
+ }),
35
+ });
36
+ }
37
+ catch (err) {
38
+ throw new Error(`OpenClaw is not reachable on port ${this.opts.openClawPort}. ` +
39
+ `Make sure it is running with chatCompletions enabled. (${String(err)})`);
40
+ }
41
+ if (!response.ok) {
42
+ throw new Error(`OpenClaw error: ${response.status} ${response.statusText}`);
43
+ }
44
+ const reader = response.body.getReader();
45
+ const decoder = new TextDecoder();
46
+ let buffer = '';
47
+ while (true) {
48
+ const { done, value } = await reader.read();
49
+ if (done)
50
+ break;
51
+ buffer += decoder.decode(value, { stream: true });
52
+ const lines = buffer.split('\n');
53
+ buffer = lines.pop() ?? '';
54
+ for (const line of lines) {
55
+ if (!line.startsWith('data: '))
56
+ continue;
57
+ const data = line.slice(6).trim();
58
+ if (data === '[DONE]')
59
+ continue;
60
+ try {
61
+ const chunk = JSON.parse(data);
62
+ const token = chunk.choices?.[0]?.delta?.content;
63
+ if (token) {
64
+ eventBus.publish({
65
+ kind: 'artifact-update',
66
+ taskId,
67
+ contextId,
68
+ artifact: {
69
+ artifactId: 'response',
70
+ parts: [{ kind: 'text', text: token }],
71
+ },
72
+ });
73
+ }
74
+ }
75
+ catch {
76
+ // ignore malformed SSE lines
77
+ }
78
+ }
79
+ }
80
+ eventBus.publish({
81
+ kind: 'status-update',
82
+ taskId,
83
+ contextId,
84
+ status: { state: 'completed', timestamp: new Date().toISOString() },
85
+ final: true,
86
+ });
87
+ eventBus.finished();
88
+ }
89
+ cancelTask = async () => { };
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // Command
93
+ // ---------------------------------------------------------------------------
94
+ export const serveCommand = new Command('serve')
95
+ .description('Start a local A2A server for your agent')
96
+ .option('--port <port>', 'Port to listen on', '4000')
97
+ .option('--url <url>', 'Public URL (e.g. from ngrok) shown in logs and AgentCard')
98
+ .option('--openclaw-port <port>', 'OpenClaw gateway port', '18789')
99
+ .option('--openclaw-token <token>', 'OpenClaw gateway auth token (or set OPENCLAW_GATEWAY_TOKEN)')
100
+ .option('--openclaw-agent <id>', 'OpenClaw agent ID to target', 'main')
101
+ .action(async (opts) => {
102
+ // 1. Must be logged into web42
103
+ try {
104
+ requireAuth();
105
+ }
106
+ catch {
107
+ console.error(chalk.red('Not authenticated. Run `web42 auth login` first.'));
108
+ process.exit(1);
109
+ }
110
+ const cwd = process.cwd();
111
+ const port = parseInt(opts.port, 10);
112
+ const openClawPort = parseInt(opts.openclawPort, 10);
113
+ const openClawToken = opts.openclawToken ?? process.env.OPENCLAW_GATEWAY_TOKEN ?? '';
114
+ const openClawAgent = opts.openclawAgent;
115
+ const publicUrl = opts.url;
116
+ // 2. Read manifest.json
117
+ const manifestPath = join(cwd, 'manifest.json');
118
+ if (!existsSync(manifestPath)) {
119
+ console.error(chalk.red('No manifest.json found in current directory.'));
120
+ console.error(chalk.dim('Run `web42 init` to create one.'));
121
+ process.exit(1);
122
+ }
123
+ let manifest;
124
+ try {
125
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
126
+ }
127
+ catch {
128
+ console.error(chalk.red('Failed to parse manifest.json.'));
129
+ process.exit(1);
130
+ }
131
+ if (!manifest.name) {
132
+ console.error(chalk.red('manifest.json must have a "name" field.'));
133
+ process.exit(1);
134
+ }
135
+ const config = getConfig();
136
+ const web42ApiUrl = config.apiUrl ?? 'https://web42.ai';
137
+ const spinner = ora('Starting A2A server...').start();
138
+ // 3. Build AgentCard from manifest
139
+ const agentCard = {
140
+ name: manifest.name,
141
+ description: manifest.description ?? '',
142
+ protocolVersion: '0.3.0',
143
+ version: manifest.version ?? '1.0.0',
144
+ url: `${publicUrl ?? `http://localhost:${port}`}/a2a/jsonrpc`,
145
+ skills: (manifest.skills ?? []).map((s) => ({
146
+ id: s.name.toLowerCase().replace(/\s+/g, '-'),
147
+ name: s.name,
148
+ description: s.description ?? '',
149
+ tags: [],
150
+ })),
151
+ capabilities: {
152
+ streaming: true,
153
+ pushNotifications: false,
154
+ },
155
+ defaultInputModes: ['text'],
156
+ defaultOutputModes: ['text'],
157
+ securitySchemes: {
158
+ Web42Bearer: { type: 'http', scheme: 'bearer' },
159
+ },
160
+ security: [{ Web42Bearer: [] }],
161
+ };
162
+ // 4. Start Express server
163
+ const app = express();
164
+ // Auth: validate caller's web42 Bearer token against marketplace introspect endpoint
165
+ const userBuilder = async (req) => {
166
+ const token = req.headers.authorization?.split(' ')[1];
167
+ if (!token)
168
+ throw new Error('Missing token');
169
+ const res = await fetch(`${web42ApiUrl}/api/auth/introspect`, {
170
+ method: 'POST',
171
+ headers: { 'Content-Type': 'application/json' },
172
+ body: JSON.stringify({ token }),
173
+ });
174
+ if (!res.ok)
175
+ throw new Error('Introspect call failed');
176
+ const result = (await res.json());
177
+ if (!result.active)
178
+ throw new Error('Unauthorized');
179
+ const userId = result.sub ?? '';
180
+ return {
181
+ get isAuthenticated() { return true; },
182
+ get userName() { return userId; },
183
+ };
184
+ };
185
+ const executor = new OpenClawAgentExecutor({ openClawPort, openClawToken, openClawAgent });
186
+ const requestHandler = new DefaultRequestHandler(agentCard, new InMemoryTaskStore(), executor);
187
+ // 5. Mount A2A SDK handlers
188
+ app.use('/.well-known/agent-card.json', agentCardHandler({ agentCardProvider: requestHandler }));
189
+ app.use('/a2a/jsonrpc', jsonRpcHandler({ requestHandler, userBuilder }));
190
+ // 6. Start listening
191
+ app.listen(port, () => {
192
+ spinner.stop();
193
+ console.log(chalk.green(`\n✓ Agent "${manifest.name}" is live`));
194
+ console.log(chalk.dim(` Local: http://localhost:${port}`));
195
+ if (publicUrl)
196
+ console.log(chalk.dim(` Public: ${publicUrl}`));
197
+ console.log(chalk.dim(` Agent card: http://localhost:${port}/.well-known/agent-card.json`));
198
+ console.log(chalk.dim(` JSON-RPC: http://localhost:${port}/a2a/jsonrpc`));
199
+ console.log(chalk.dim('\nWaiting for requests... (Ctrl+C to stop)\n'));
200
+ });
201
+ // 7. Keep process alive
202
+ process.on('SIGINT', () => {
203
+ console.log(chalk.dim('\nShutting down...'));
204
+ process.exit(0);
205
+ });
206
+ });
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ import { pushCommand } from "./commands/push.js";
8
8
  import { pullCommand } from "./commands/pull.js";
9
9
  import { remixCommand } from "./commands/remix.js";
10
10
  import { searchCommand } from "./commands/search.js";
11
+ import { sendCommand } from "./commands/send.js";
12
+ import { serveCommand } from "./commands/serve.js";
11
13
  import { syncCommand } from "./commands/sync.js";
12
14
  import { getAllPlatformCommands } from "./platforms/registry.js";
13
15
  import { setApiUrl } from "./utils/config.js";
@@ -32,6 +34,8 @@ program.addCommand(pushCommand);
32
34
  program.addCommand(pullCommand);
33
35
  program.addCommand(remixCommand);
34
36
  program.addCommand(searchCommand);
37
+ program.addCommand(sendCommand);
38
+ program.addCommand(serveCommand);
35
39
  program.addCommand(syncCommand);
36
40
  for (const platformCmd of getAllPlatformCommands()) {
37
41
  program.addCommand(platformCmd);
@@ -9,6 +9,7 @@ export interface PackOptions {
9
9
  cwd: string;
10
10
  outputDir: string;
11
11
  dryRun?: boolean;
12
+ agentName?: string;
12
13
  }
13
14
  export interface PackedFile {
14
15
  path: string;
@@ -32,6 +33,7 @@ export interface InstallOptions {
32
33
  }>;
33
34
  configTemplate: Record<string, unknown> | null;
34
35
  configAnswers: Record<string, string>;
36
+ version?: string;
35
37
  }
36
38
  export interface InstallResult {
37
39
  filesWritten: number;
@@ -39,6 +41,7 @@ export interface InstallResult {
39
41
  }
40
42
  export interface UninstallOptions {
41
43
  agentName: string;
44
+ workspacePath?: string;
42
45
  }
43
46
  export interface UninstallResult {
44
47
  removed: boolean;
@@ -53,6 +56,18 @@ export interface InitConfig {
53
56
  name: string;
54
57
  model?: string;
55
58
  }
59
+ export interface AgentCandidate {
60
+ name: string;
61
+ description?: string;
62
+ model?: string;
63
+ skills: string[];
64
+ path: string;
65
+ }
66
+ export interface ResolvedSkill {
67
+ name: string;
68
+ sourcePath: string;
69
+ found: boolean;
70
+ }
56
71
  export interface PlatformAdapter {
57
72
  name: string;
58
73
  home: string;
@@ -61,4 +76,7 @@ export interface PlatformAdapter {
61
76
  install(options: InstallOptions): Promise<InstallResult>;
62
77
  uninstall(options: UninstallOptions): Promise<UninstallResult>;
63
78
  listInstalled(): Promise<InstalledAgent[]>;
79
+ discoverAgents?(cwd: string): AgentCandidate[];
80
+ resolveSkills?(skillNames: string[], cwd: string): ResolvedSkill[];
81
+ resolveInstallPath?(localName: string, global?: boolean): string;
64
82
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,257 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdirSync, writeFileSync, rmSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { ClaudeAdapter } from "../adapter.js";
6
+ function createTempDir() {
7
+ const dir = join(tmpdir(), `claude-adapter-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
8
+ mkdirSync(dir, { recursive: true });
9
+ return dir;
10
+ }
11
+ describe("ClaudeAdapter", () => {
12
+ let adapter;
13
+ let tempDir;
14
+ beforeEach(() => {
15
+ adapter = new ClaudeAdapter();
16
+ tempDir = createTempDir();
17
+ // Override home so discoverAgents/resolveSkills don't pick up the real
18
+ // ~/.claude directory during tests, making results fully deterministic.
19
+ adapter.home = tempDir;
20
+ });
21
+ afterEach(() => {
22
+ if (existsSync(tempDir)) {
23
+ rmSync(tempDir, { recursive: true, force: true });
24
+ }
25
+ });
26
+ describe("discoverAgents", () => {
27
+ it("finds agents in cwd/agents/", () => {
28
+ const agentsDir = join(tempDir, "agents");
29
+ mkdirSync(agentsDir, { recursive: true });
30
+ writeFileSync(join(agentsDir, "test-agent.md"), `---
31
+ name: test-agent
32
+ description: A test agent
33
+ model: sonnet
34
+ skills:
35
+ - skill-one
36
+ ---
37
+
38
+ You are a test agent.
39
+ `);
40
+ const agents = adapter.discoverAgents(tempDir);
41
+ expect(agents).toHaveLength(1);
42
+ expect(agents[0].name).toBe("test-agent");
43
+ expect(agents[0].description).toBe("A test agent");
44
+ expect(agents[0].model).toBe("sonnet");
45
+ expect(agents[0].skills).toEqual(["skill-one"]);
46
+ });
47
+ it("parses frontmatter correctly with multiple skills", () => {
48
+ const agentsDir = join(tempDir, "agents");
49
+ mkdirSync(agentsDir, { recursive: true });
50
+ writeFileSync(join(agentsDir, "multi-skill.md"), `---
51
+ name: multi-skill-agent
52
+ description: Agent with many skills
53
+ model: opus
54
+ skills:
55
+ - flutter-conventions
56
+ - dart-testing
57
+ - a11y-flutter
58
+ tools: Read, Grep, Glob, Bash
59
+ ---
60
+
61
+ Body content.
62
+ `);
63
+ const agents = adapter.discoverAgents(tempDir);
64
+ expect(agents).toHaveLength(1);
65
+ expect(agents[0].skills).toEqual([
66
+ "flutter-conventions",
67
+ "dart-testing",
68
+ "a11y-flutter",
69
+ ]);
70
+ });
71
+ it("returns empty for no agents", () => {
72
+ const agents = adapter.discoverAgents(tempDir);
73
+ expect(agents).toEqual([]);
74
+ });
75
+ it("uses filename as fallback name when no frontmatter name", () => {
76
+ const agentsDir = join(tempDir, "agents");
77
+ mkdirSync(agentsDir, { recursive: true });
78
+ writeFileSync(join(agentsDir, "my-agent.md"), `---
79
+ description: No name in frontmatter
80
+ ---
81
+
82
+ Body.
83
+ `);
84
+ const agents = adapter.discoverAgents(tempDir);
85
+ expect(agents[0].name).toBe("my-agent");
86
+ });
87
+ it("discovers multiple agents", () => {
88
+ const agentsDir = join(tempDir, "agents");
89
+ mkdirSync(agentsDir, { recursive: true });
90
+ writeFileSync(join(agentsDir, "agent-a.md"), "---\nname: agent-a\n---\nBody.");
91
+ writeFileSync(join(agentsDir, "agent-b.md"), "---\nname: agent-b\n---\nBody.");
92
+ const agents = adapter.discoverAgents(tempDir);
93
+ expect(agents).toHaveLength(2);
94
+ const names = agents.map((a) => a.name).sort();
95
+ expect(names).toEqual(["agent-a", "agent-b"]);
96
+ });
97
+ it("skips non-.md files", () => {
98
+ const agentsDir = join(tempDir, "agents");
99
+ mkdirSync(agentsDir, { recursive: true });
100
+ writeFileSync(join(agentsDir, "agent.md"), "---\nname: real-agent\n---\nBody.");
101
+ writeFileSync(join(agentsDir, "notes.txt"), "Just a text file");
102
+ writeFileSync(join(agentsDir, ".DS_Store"), "");
103
+ const agents = adapter.discoverAgents(tempDir);
104
+ expect(agents).toHaveLength(1);
105
+ expect(agents[0].name).toBe("real-agent");
106
+ });
107
+ });
108
+ describe("resolveSkills", () => {
109
+ it("finds skills in cwd/skills/", () => {
110
+ const skillDir = join(tempDir, "skills", "my-skill");
111
+ mkdirSync(skillDir, { recursive: true });
112
+ writeFileSync(join(skillDir, "SKILL.md"), "# My Skill\nDescription here.");
113
+ const resolved = adapter.resolveSkills(["my-skill"], tempDir);
114
+ expect(resolved).toHaveLength(1);
115
+ expect(resolved[0].name).toBe("my-skill");
116
+ expect(resolved[0].found).toBe(true);
117
+ expect(resolved[0].sourcePath).toBe(skillDir);
118
+ });
119
+ it("marks missing skills as not found", () => {
120
+ const resolved = adapter.resolveSkills(["nonexistent-skill"], tempDir);
121
+ expect(resolved).toHaveLength(1);
122
+ expect(resolved[0].found).toBe(false);
123
+ });
124
+ it("resolves multiple skills", () => {
125
+ mkdirSync(join(tempDir, "skills", "skill-a"), { recursive: true });
126
+ writeFileSync(join(tempDir, "skills", "skill-a", "SKILL.md"), "# Skill A");
127
+ mkdirSync(join(tempDir, "skills", "skill-b"), { recursive: true });
128
+ writeFileSync(join(tempDir, "skills", "skill-b", "SKILL.md"), "# Skill B");
129
+ const resolved = adapter.resolveSkills(["skill-a", "skill-b", "missing"], tempDir);
130
+ expect(resolved.filter((s) => s.found)).toHaveLength(2);
131
+ expect(resolved.find((s) => s.name === "missing")?.found).toBe(false);
132
+ });
133
+ });
134
+ describe("extractInitConfig", () => {
135
+ it("returns config when agent exists", () => {
136
+ const agentsDir = join(tempDir, "agents");
137
+ mkdirSync(agentsDir, { recursive: true });
138
+ writeFileSync(join(agentsDir, "test.md"), "---\nname: test-agent\nmodel: sonnet\n---\nBody.");
139
+ const config = adapter.extractInitConfig(tempDir);
140
+ expect(config).not.toBeNull();
141
+ expect(config.name).toBe("test-agent");
142
+ expect(config.model).toBe("sonnet");
143
+ });
144
+ it("returns null when no agents exist", () => {
145
+ const config = adapter.extractInitConfig(tempDir);
146
+ expect(config).toBeNull();
147
+ });
148
+ });
149
+ describe("pack", () => {
150
+ it("collects agent and referenced skills", async () => {
151
+ // Set up agent
152
+ const agentsDir = join(tempDir, "agents");
153
+ mkdirSync(agentsDir, { recursive: true });
154
+ writeFileSync(join(agentsDir, "my-agent.md"), `---
155
+ name: my-agent
156
+ skills:
157
+ - test-skill
158
+ ---
159
+
160
+ Agent body.
161
+ `);
162
+ // Set up skill
163
+ const skillDir = join(tempDir, "skills", "test-skill");
164
+ mkdirSync(skillDir, { recursive: true });
165
+ writeFileSync(join(skillDir, "SKILL.md"), "# Test Skill\nSkill description.");
166
+ mkdirSync(join(skillDir, "references"), { recursive: true });
167
+ writeFileSync(join(skillDir, "references", "ref.md"), "Reference content.");
168
+ const result = await adapter.pack({
169
+ cwd: tempDir,
170
+ outputDir: ".web42/my-agent/dist",
171
+ agentName: "my-agent",
172
+ });
173
+ expect(result.files.length).toBeGreaterThanOrEqual(3);
174
+ const paths = result.files.map((f) => f.path);
175
+ expect(paths).toContain("agents/my-agent.md");
176
+ expect(paths).toContain("skills/test-skill/SKILL.md");
177
+ expect(paths).toContain("skills/test-skill/references/ref.md");
178
+ });
179
+ it("excludes .git and node_modules", async () => {
180
+ const agentsDir = join(tempDir, "agents");
181
+ mkdirSync(agentsDir, { recursive: true });
182
+ writeFileSync(join(agentsDir, "test.md"), "---\nname: test\n---\nBody.");
183
+ // Create files that should be excluded
184
+ mkdirSync(join(tempDir, ".git"), { recursive: true });
185
+ writeFileSync(join(tempDir, ".git", "config"), "git config");
186
+ mkdirSync(join(tempDir, "node_modules", "pkg"), { recursive: true });
187
+ writeFileSync(join(tempDir, "node_modules", "pkg", "index.js"), "module.exports = {}");
188
+ const result = await adapter.pack({
189
+ cwd: tempDir,
190
+ outputDir: ".web42/test/dist",
191
+ agentName: "test",
192
+ });
193
+ const paths = result.files.map((f) => f.path);
194
+ expect(paths.some((p) => p.includes(".git"))).toBe(false);
195
+ expect(paths.some((p) => p.includes("node_modules"))).toBe(false);
196
+ });
197
+ it("applies template var sanitization", async () => {
198
+ const agentsDir = join(tempDir, "agents");
199
+ mkdirSync(agentsDir, { recursive: true });
200
+ writeFileSync(join(agentsDir, "test.md"), "---\nname: test\n---\nPath: ~/.claude/agents/test.md");
201
+ const result = await adapter.pack({
202
+ cwd: tempDir,
203
+ outputDir: ".web42/test/dist",
204
+ agentName: "test",
205
+ });
206
+ const agentFile = result.files.find((f) => f.path === "agents/test.md");
207
+ expect(agentFile?.content).toContain("{{CLAUDE_HOME}}");
208
+ expect(agentFile?.content).not.toContain("~/.claude/");
209
+ });
210
+ it("throws when agentName is not provided", async () => {
211
+ await expect(adapter.pack({ cwd: tempDir, outputDir: "dist" })).rejects.toThrow("agentName");
212
+ });
213
+ it("throws when agent file not found", async () => {
214
+ await expect(adapter.pack({ cwd: tempDir, outputDir: "dist", agentName: "nonexistent" })).rejects.toThrow("not found");
215
+ });
216
+ it("collects commands if present", async () => {
217
+ const agentsDir = join(tempDir, "agents");
218
+ mkdirSync(agentsDir, { recursive: true });
219
+ writeFileSync(join(agentsDir, "test.md"), "---\nname: test\n---\nBody.");
220
+ const commandsDir = join(tempDir, "commands");
221
+ mkdirSync(commandsDir, { recursive: true });
222
+ writeFileSync(join(commandsDir, "review.md"), "# Review command");
223
+ const result = await adapter.pack({
224
+ cwd: tempDir,
225
+ outputDir: "dist",
226
+ agentName: "test",
227
+ });
228
+ const paths = result.files.map((f) => f.path);
229
+ expect(paths).toContain("commands/review.md");
230
+ });
231
+ });
232
+ describe("install and uninstall", () => {
233
+ let installDir;
234
+ beforeEach(() => {
235
+ installDir = createTempDir();
236
+ });
237
+ afterEach(() => {
238
+ if (existsSync(installDir)) {
239
+ rmSync(installDir, { recursive: true, force: true });
240
+ }
241
+ });
242
+ it("rejects path traversal", async () => {
243
+ // We can't easily test the actual install since it writes to ~/.claude/
244
+ // but we can test the path traversal check by examining the adapter logic
245
+ const files = [
246
+ { path: "../../../etc/passwd", content: "bad content", content_hash: "abc" },
247
+ ];
248
+ // The adapter checks: resolve(CLAUDE_HOME, file.path).startsWith(resolve(CLAUDE_HOME))
249
+ // A path with ../ would resolve outside CLAUDE_HOME
250
+ const { resolve: pathResolve } = await import("path");
251
+ const { homedir: getHomedir } = await import("os");
252
+ const home = join(getHomedir(), ".claude");
253
+ const resolved = pathResolve(home, "../../../etc/passwd");
254
+ expect(resolved.startsWith(pathResolve(home))).toBe(false);
255
+ });
256
+ });
257
+ });
@@ -0,0 +1 @@
1
+ export {};