@teammy/cli 0.0.10 → 0.1.0

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
@@ -2,10 +2,12 @@
2
2
 
3
3
  成员设备上的命令行客户端,需要 Node.js 22.13+,推荐 Node.js 24。
4
4
 
5
- 0.0.10 与服务端保持版本一致,支持多项目后台同步、根 README / `_template/` 公共资料分发,以及 `workspaces/.teammy-local.json` 中的 `files.ignore` 忽略规则。规则相对本人目录,下一轮检查生效,不删除已共享历史。公共资料由项目设置指定的一位成员维护。自动任务整理在服务端运行。
5
+ 0.1.0 提供完整目录发布、后台升级修复、Hook 清理与新增 Agent 采集支持,需与配套服务端一起升级。
6
+
7
+ 客户端与服务端保持配套版本,支持多项目后台同步、根 README / `_template/` 公共资料分发,以及 `workspaces/.teammy-local.json` 中的 `files.ignore` 忽略规则。规则相对本人目录,下一轮检查生效,不删除已共享历史。公共资料由项目设置指定的一位成员维护。自动任务整理在服务端运行。
6
8
 
7
9
  ```sh
8
- npm install -g @teammy/cli@0.0.10
10
+ npm install -g @teammy/cli@0.1.0
9
11
  ```
10
12
 
11
13
  ```sh
@@ -22,7 +24,7 @@ teammy logs
22
24
 
23
25
  本人本地文件是发布来源,服务器旧正文不会下载到本人目录。首次本地缺失的历史文件保留在服务器,不推断为删除;确认曾在本地存在的文件,之后删除并完成稳定等待才传播。其他成员目录只接收,不反向发布,本地误改会保留并报告。
24
26
 
25
- 默认约一分钟检查一次,每个文件连续一分钟没有变化后发布;首次下载立即开始,大批量接收自动续批。周期由全站网页设置热更新。项目会话通过 hook 及后台补扫自动登记、提取并上传;只采集属于当前项目目录的会话,不自动扩展到 monorepo 上层其他会话。已有接入可通过 `sync on` 安装本项目 hook。
27
+ 默认约一分钟检查一次,文件变化连续一分钟稳定后准备完整目录版本;内容可分批传输,完整版本发布后才对外可见并触发任务整理。首次下载立即开始,大批量接收自动续批。周期由全站网页设置热更新。项目会话通过 hook 及后台补扫自动登记、提取并上传;只采集属于当前项目目录的会话,不自动扩展到 monorepo 上层其他会话。已有接入可通过 `sync on` 安装本项目 hook。
26
28
 
27
29
  macOS 使用用户 LaunchAgent,Linux 使用 systemd 用户服务;不安装系统级服务、不启用 lingering。登录会话没有可用服务管理器的环境可用 `connect --no-start` 后运行 `sync --foreground`,此方式需保持终端运行。Windows 暂不支持自动后台服务。
28
30
 
@@ -32,8 +34,32 @@ macOS 使用用户 LaunchAgent,Linux 使用 systemd 用户服务;不安装
32
34
 
33
35
  `sync --once` 执行一轮并可复用后台进程;不保证文件已稳定或所有批次完成。需要手动覆盖时,先 off,再使用 `sync --once --settle-ms 0`、`--restore-missing`、`--retry` 等参数。`--restore-missing` 只恢复缺失的同事副本,不覆盖现有修改。`--adopt-baseline` 保留为显式核对本人本地文件与历史一致性的工具,正常接入不需要。管理员重置设备后,先 off,再用原 connect 命令加 `--reconnect`,保留同步基线。
34
36
 
35
- 已接入目录不能静默改绑;切换项目需指定另一个项目根。若修改 Node.js 或 CLI 安装位置,重新运行 sync on 可更新该用户服务的启动路径。服务依赖安装路径持续存在,不能把临时安装目录当作长期运行环境。
37
+ 已接入目录不能静默改绑;切换项目需指定另一个项目根。若修改 Node.js 或 CLI 安装位置,运行 `teammy repair` 更新所有绑定项目的 Hook 和后台入口。服务依赖安装路径持续存在,不能把临时安装目录当作长期运行环境。
38
+
39
+ 对话库汇集成员与 Agent 的工作对话,支持 Claude Code / tclaude、Codex / tcodex、Cursor CLI SQLite / 远程 transcript JSONL、Pi v3(用户级被动采集扩展)、DSH v0。连接或开启同步时合并项目 hook,保留已有用户配置;hook 只登记本地线索,不等待网络,后台统一上传用户与 assistant 正文。`session list`、`session search --query TEXT`、`session read --owner MEMBER --key KEY` 用于查询对话库;`session sources` 查看本机来源;详见 `teammy session --help`。产品名称更新不改变 `session` 子命令。
40
+
41
+ ## 更新与卸载
42
+
43
+ 本次文件发布协议依赖服务端 `atomic-file-publications` 能力,两端需配套更新。以下维护命令从 0.1.0 提供,先安装新版,再执行修复:
44
+
45
+ ```sh
46
+ npm install -g @teammy/cli@0.1.0
47
+ teammy repair
48
+ ```
49
+
50
+ `repair` 替换后台并校准全部绑定项目的 Hook,保留身份、队列和各项目 on/off 状态;同版本重装也可执行。不要求旧服务先成功停止,不依赖服务器在线。仅安装 npm 包不会替换已经运行的进程,因此请执行第二条命令。
51
+
52
+ `teammy repair --stop` 持久暂停整个本机后台;显式 `repair` 或 `sync on` 恢复。`status` 和 `logs --all` 在业务库异常时仍提供可用诊断。不要通过清空状态目录处理普通升级问题。
53
+
54
+ ```sh
55
+ teammy session teardown --all # 仅移除 Hook,目录发现继续工作
56
+ teammy session setup # 显式恢复当前项目 Hook
57
+ teammy uninstall # 移除后台服务和所有已登记 Hook,保留用户数据
58
+ npm uninstall -g @teammy/cli
59
+ ```
60
+
61
+ 上述命令支持 `--state-dir`;多个独立状态目录需分别清理。卸载 Hook 后,普通 repair/sync on 不会重新注入;显式 session setup 或重新接入才会启用。已启动的 Agent 可能缓存旧命令,需要新开会话加载配置。新版 Hook 即使遇到入口缺失、未绑定或解析失败,也静默成功,不向 prompt/system prompt 插入文本、Skill、工具或 MCP 配置。
36
62
 
37
- 会话支持 Claude Code、Codex、Cursor CLI SQLite / 远程 transcript JSONL、Pi v3、DSH v0。连接或开启同步时合并项目 hook,保留已有用户配置;hook 只登记本地线索,不等待网络,后台统一上传用户与 assistant 正文。`session list`、`session search --query TEXT`、`session read --owner MEMBER --key KEY` 用于查看团队会话;`session sources` 查看本机来源;详见 `teammy session --help`。
63
+ Pi 扩展随连接和修复自动维护,支持自定义 profile 与会话路径;仅采集已绑定项目,不增加模型工具或上下文。`--no-session` 不采集正文。多个项目共享扩展,卸载最后一个绑定的 Hook 后移除扩展文件。最新版使用最近落盘分支,已上传的旧版本保留搜索。
38
64
 
39
65
  服务器单独安装 `@teammy/server`。`server-info --url URL` 只查询能力与版本,以 JSON 返回,不创建绑定。许可证附于 `dist/LICENSE`。
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ // This entry does not load the CLI or SQLite. The registered shell command also
4
+ // protects missing Node/package files before JavaScript can run.
5
+ const deadline = setTimeout(() => process.exit(0), 1500);
6
+ try {
7
+ const { runHook } = await import('../dist/hook.js');
8
+ await runHook(process.argv.slice(process.argv[2] === 'session' ? 4 : 2));
9
+ } catch {
10
+ // Hook diagnostics must never become host-visible stdout, stderr or exit codes.
11
+ } finally {
12
+ clearTimeout(deadline);
13
+ process.exit(0);
14
+ }
package/bin/teammy.js CHANGED
@@ -1,6 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync } from 'node:fs';
3
+ import { readFileSync, realpathSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ if (process.argv[2] === 'session' && process.argv[3] === 'hook') {
6
+ await import('./session-hook.js').catch(() => process.exit(0));
7
+ }
8
+ // npm's public executable is a symlink. Persist the package entry, so service
9
+ // definitions and the sibling Hook entry remain valid when launched via PATH.
10
+ process.argv[1] = realpathSync(fileURLToPath(import.meta.url));
4
11
  // SQLite's known experimental notice is not an actionable CLI diagnostic.
5
12
  const warningHandlers = process.listeners('warning');
6
13
  process.removeAllListeners('warning');
package/dist/hook.js ADDED
@@ -0,0 +1,340 @@
1
+ // apps/client/sessions/hook-entry.ts
2
+ import { parseArgs } from "node:util";
3
+ import { realpath } from "node:fs/promises";
4
+ import { dirname, isAbsolute as isAbsolute2, relative, resolve, sep } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { join as join3 } from "node:path";
7
+
8
+ // apps/client/sessions/registration.ts
9
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
10
+ import { join } from "node:path";
11
+
12
+ // apps/client/runtime/log.ts
13
+ import { createHash } from "node:crypto";
14
+ function rootKey(root) {
15
+ return createHash("sha256").update(root).digest("hex").slice(0, 16);
16
+ }
17
+
18
+ // apps/client/sessions/registration.ts
19
+ var registrationPath = (directory, root) => join(directory, "hook-registrations", rootKey(root) + ".json");
20
+ function readRegistration(directory, root) {
21
+ const path = registrationPath(directory, root);
22
+ if (!existsSync(path)) return null;
23
+ const value = JSON.parse(readFileSync(path, "utf8"));
24
+ if (value.format !== 1 || value.root !== root || !Array.isArray(value.configs))
25
+ throw new Error("Invalid Hook registration.");
26
+ return value;
27
+ }
28
+ function registrations(directory, onError) {
29
+ const path = join(directory, "hook-registrations");
30
+ if (!existsSync(path)) return [];
31
+ return readdirSync(path).filter((name) => /^[a-f0-9]{16}\.json$/.test(name)).flatMap((name) => {
32
+ try {
33
+ const value = JSON.parse(readFileSync(join(path, name), "utf8"));
34
+ const registration = readRegistration(directory, value.root);
35
+ if (!registration) throw new Error("Missing Hook registration.");
36
+ return [registration];
37
+ } catch (error) {
38
+ onError(join(path, name), error);
39
+ return [];
40
+ }
41
+ });
42
+ }
43
+
44
+ // apps/client/sessions/hooks.ts
45
+ import { createHash as createHash2, randomUUID } from "node:crypto";
46
+ import { mkdir, open, opendir, rename, unlink } from "node:fs/promises";
47
+ import { isAbsolute, join as join2 } from "node:path";
48
+ import { z as z5 } from "zod";
49
+
50
+ // packages/protocol/sessions.ts
51
+ import { z as z4 } from "zod";
52
+
53
+ // packages/protocol/files.ts
54
+ import { z } from "zod";
55
+ var MAX_BLOB_BYTES = 128 * 1024 * 1024;
56
+ var MAX_FILE_BYTES = 10 * 1024 * 1024;
57
+ var MAX_BATCH_FILES = 200;
58
+ var HashSchema = z.string().regex(/^[a-f0-9]{64}$/);
59
+ var RevisionSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
60
+ var FilePathSchema = z.string().min(1).max(1024).refine((path) => {
61
+ if (path !== path.normalize("NFC") || /[\\\x00-\x1f\x7f:<>"|?*]/.test(path)) return false;
62
+ return path.split("/").every(
63
+ (part) => part.length > 0 && part.length <= 255 && part !== "." && part !== ".." && !/[. ]$/.test(part) && !/^\.teammy(?:-|$)/i.test(part) && !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)
64
+ );
65
+ }, "Path must be a portable, normalized relative file path.");
66
+ var BlobRefSchema = z.object({
67
+ sha256: HashSchema,
68
+ size: z.number().int().min(0).max(MAX_BLOB_BYTES)
69
+ });
70
+ var FileBlobRefSchema = BlobRefSchema.extend({
71
+ size: z.number().int().min(0).max(MAX_FILE_BYTES)
72
+ });
73
+ var FileChangeSchema = z.discriminatedUnion("kind", [
74
+ FileBlobRefSchema.extend({ kind: z.literal("upsert"), path: FilePathSchema }).strict(),
75
+ z.object({ kind: z.literal("delete"), path: FilePathSchema }).strict()
76
+ ]);
77
+ var FileCommitSchema = z.object({
78
+ operationId: z.uuid(),
79
+ parentRevision: RevisionSchema,
80
+ changes: z.array(FileChangeSchema)
81
+ }).strict().refine(
82
+ (batch) => new Set(batch.changes.map((change) => change.path)).size === batch.changes.length,
83
+ "Duplicate paths in a publication."
84
+ );
85
+ var CommitResultSchema = z.object({ operationId: z.uuid(), revision: RevisionSchema });
86
+ var FileEntrySchema = FileBlobRefSchema.extend({
87
+ path: FilePathSchema,
88
+ revision: RevisionSchema
89
+ });
90
+ var ManifestSchema = z.object({
91
+ revision: RevisionSchema,
92
+ complete: z.boolean().default(false),
93
+ files: z.array(FileEntrySchema),
94
+ nextCursor: z.string().nullable()
95
+ });
96
+ var FileRevisionSchema = z.object({
97
+ revision: RevisionSchema,
98
+ operationId: z.uuid(),
99
+ createdAt: z.string(),
100
+ changes: z.array(FileChangeSchema)
101
+ });
102
+ var ChangesSchema = z.object({
103
+ headRevision: RevisionSchema,
104
+ revisions: z.array(FileRevisionSchema)
105
+ });
106
+ var BlobCheckSchema = z.object({ hashes: z.array(HashSchema).max(MAX_BATCH_FILES) }).strict();
107
+ var MissingBlobsSchema = z.object({ missing: z.array(HashSchema) });
108
+ var FileTreeEntrySchema = z.discriminatedUnion("kind", [
109
+ FileEntrySchema.extend({ kind: z.literal("file") }),
110
+ z.object({
111
+ kind: z.literal("directory"),
112
+ path: FilePathSchema,
113
+ fileCount: z.number().int().positive()
114
+ })
115
+ ]);
116
+ var FileTreeSchema = z.object({
117
+ revision: RevisionSchema,
118
+ path: z.union([z.literal(""), FilePathSchema]),
119
+ entries: z.array(FileTreeEntrySchema),
120
+ nextCursor: z.string().nullable()
121
+ });
122
+ var FileSelectionSchema = z.object({ revision: RevisionSchema, file: FileEntrySchema });
123
+
124
+ // packages/protocol/identity.ts
125
+ import { z as z2 } from "zod";
126
+ var NameSchema = z2.string().regex(/^[a-z][a-z0-9_-]{0,63}$/);
127
+ var DeviceIdSchema = z2.uuid();
128
+ var BindRequestSchema = z2.object({ deviceId: DeviceIdSchema, projectId: NameSchema.optional() }).strict();
129
+ var BindingSchema = z2.object({
130
+ workspaceId: NameSchema,
131
+ memberId: NameSchema,
132
+ deviceId: DeviceIdSchema
133
+ });
134
+ var MembersSchema = z2.object({ members: z2.array(NameSchema) });
135
+
136
+ // packages/protocol/session-analysis.ts
137
+ import { z as z3 } from "zod";
138
+ var SessionAnalysisKindSchema = z3.enum(["overview", "handoff"]);
139
+ var SessionOverviewOutputSchema = z3.object({ title: z3.string().trim().min(1), summary: z3.string().trim().min(1) }).strict();
140
+ var SessionHandoffOutputSchema = z3.object({ markdown: z3.string().trim().min(1) }).strict();
141
+ var SessionOverviewSchema = SessionOverviewOutputSchema.extend({
142
+ sourceRevision: RevisionSchema,
143
+ sourceHash: HashSchema,
144
+ generatedAt: z3.string(),
145
+ stale: z3.boolean()
146
+ });
147
+ var SessionAnalysisStateSchema = z3.object({
148
+ kind: SessionAnalysisKindSchema,
149
+ status: z3.enum(["absent", "queued", "running", "succeeded", "failed"]),
150
+ available: z3.boolean(),
151
+ currentRevision: RevisionSchema,
152
+ sourceRevision: RevisionSchema.nullable(),
153
+ stale: z3.boolean(),
154
+ result: z3.union([SessionOverviewOutputSchema, SessionHandoffOutputSchema]).nullable(),
155
+ generatedAt: z3.string().nullable(),
156
+ error: z3.string().nullable()
157
+ });
158
+
159
+ // packages/protocol/sessions.ts
160
+ var AgentSchema = z4.enum(["claude-code", "codex", "cursor", "pi", "dsh"]);
161
+ var SessionSnapshotSchema = z4.object({
162
+ formatVersion: z4.literal(1),
163
+ adapterVersion: z4.literal(1),
164
+ agent: AgentSchema,
165
+ nativeId: z4.string().min(1).max(512),
166
+ cwd: z4.string().min(1).max(4096),
167
+ messages: z4.array(
168
+ z4.object({
169
+ id: z4.string().min(1).max(1024),
170
+ role: z4.enum(["user", "assistant"]),
171
+ text: z4.string().min(1),
172
+ source: z4.string().min(1).max(1024)
173
+ }).strict()
174
+ )
175
+ }).strict().refine(
176
+ (s) => new Set(s.messages.map((m) => m.id)).size === s.messages.length,
177
+ "Duplicate message IDs."
178
+ );
179
+ var SessionCommitSchema = z4.object({
180
+ operationId: z4.uuid(),
181
+ sessionKey: HashSchema,
182
+ parentRevision: RevisionSchema,
183
+ blob: BlobRefSchema
184
+ }).strict();
185
+ var SessionInfoSchema = z4.object({
186
+ owner: NameSchema,
187
+ sessionKey: HashSchema,
188
+ agent: AgentSchema,
189
+ nativeId: z4.string(),
190
+ cwd: z4.string(),
191
+ title: z4.string(),
192
+ revision: RevisionSchema,
193
+ updatedAt: z4.string(),
194
+ messageCount: z4.number().int().nonnegative(),
195
+ blob: BlobRefSchema,
196
+ overview: SessionOverviewSchema.optional()
197
+ });
198
+ var SessionListSchema = z4.object({
199
+ sessions: z4.array(SessionInfoSchema),
200
+ nextCursor: RevisionSchema.nullable()
201
+ });
202
+ var SessionStatsSchema = z4.object({
203
+ totalSessions: z4.number().int().nonnegative(),
204
+ totalMessages: z4.number().int().nonnegative(),
205
+ memberCount: z4.number().int().nonnegative(),
206
+ updatedAt: z4.string().nullable()
207
+ });
208
+ var SessionScopeSchema = z4.object({
209
+ owner: NameSchema.optional(),
210
+ sessionKey: HashSchema.optional()
211
+ }).strict().refine((v) => !v.sessionKey || !!v.owner, "A session scope requires an owner.");
212
+ var SessionSearchSchema = z4.object({
213
+ matches: z4.array(
214
+ z4.object({
215
+ owner: NameSchema,
216
+ sessionKey: HashSchema,
217
+ revision: RevisionSchema,
218
+ messageId: z4.string(),
219
+ role: z4.enum(["user", "assistant"]),
220
+ snippet: z4.string(),
221
+ title: z4.string()
222
+ })
223
+ ),
224
+ nextCursor: RevisionSchema.nullable()
225
+ });
226
+
227
+ // apps/client/sessions/normalize.ts
228
+ function object(value) {
229
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
230
+ }
231
+
232
+ // apps/client/sessions/hooks.ts
233
+ var HintSchema = z5.object({
234
+ agent: AgentSchema,
235
+ path: z5.string().min(1).max(4096).refine(isAbsolute).optional(),
236
+ nativeId: z5.string().min(1).max(512).optional(),
237
+ projectDir: z5.string().min(1).max(4096).refine(isAbsolute)
238
+ });
239
+ function inbox(directory, root) {
240
+ return join2(directory, "session-hints", createHash2("sha256").update(root).digest("hex"));
241
+ }
242
+ async function recordHint(directory, root, input) {
243
+ const hint = HintSchema.parse(input);
244
+ const targetDir = inbox(directory, root);
245
+ await mkdir(targetDir, { recursive: true, mode: 448 });
246
+ const target = join2(
247
+ targetDir,
248
+ `${createHash2("sha256").update(JSON.stringify(hint)).digest("hex")}.json`
249
+ );
250
+ const temporary = `${target}.${randomUUID()}.tmp`;
251
+ const handle = await open(temporary, "wx", 384);
252
+ try {
253
+ await handle.writeFile(JSON.stringify(hint));
254
+ } finally {
255
+ await handle.close();
256
+ }
257
+ await rename(temporary, target);
258
+ }
259
+ async function hookPayload() {
260
+ let size = 0;
261
+ const parts = [];
262
+ const timeout = setTimeout(() => process.stdin.destroy(new Error("Hook input timeout.")), 500);
263
+ try {
264
+ for await (const part of process.stdin) {
265
+ const bytes = Buffer.from(part);
266
+ size += bytes.length;
267
+ if (size > 4 * 1024 * 1024) throw new Error("Hook payload too large.");
268
+ parts.push(bytes);
269
+ }
270
+ return object(JSON.parse(Buffer.concat(parts).toString("utf8")));
271
+ } finally {
272
+ clearTimeout(timeout);
273
+ }
274
+ }
275
+
276
+ // apps/client/sessions/hook-entry.ts
277
+ async function runHook(args) {
278
+ try {
279
+ const { values } = parseArgs({
280
+ args,
281
+ options: {
282
+ agent: { type: "string" },
283
+ root: { type: "string" },
284
+ "state-dir": { type: "string" },
285
+ "project-dir": { type: "string" },
286
+ file: { type: "string" },
287
+ "integration-file": { type: "string" }
288
+ }
289
+ });
290
+ const directory = await realpath(
291
+ resolve(values["state-dir"] ?? join3(homedir(), ".teammy", "client"))
292
+ );
293
+ const payload = values.file ? {} : await hookPayload();
294
+ let selectedRoot = values.root;
295
+ if (values["integration-file"]) {
296
+ if (values.agent !== "pi" || typeof payload.cwd !== "string") return;
297
+ const cwd = await realpath(payload.cwd);
298
+ const candidates = registrations(directory, () => {
299
+ }).filter(
300
+ (item) => item.enabled && item.configs.some(
301
+ (config) => config.kind === "pi-extension" && config.path === values["integration-file"]
302
+ )
303
+ );
304
+ const matches = [];
305
+ for (const item of candidates) {
306
+ try {
307
+ const project = await realpath(item.projectDir);
308
+ const rel = relative(project, cwd);
309
+ if (!isAbsolute2(rel) && rel !== ".." && !rel.startsWith(".." + sep)) matches.push(item);
310
+ } catch {
311
+ }
312
+ }
313
+ matches.sort((a, b) => b.projectDir.length - a.projectDir.length);
314
+ selectedRoot = matches[0]?.root;
315
+ if (!selectedRoot) return;
316
+ }
317
+ const root = await realpath(resolve(selectedRoot ?? "workspaces"));
318
+ const registration = readRegistration(directory, root);
319
+ if (!registration?.enabled) return;
320
+ const projectDir = await realpath(registration.projectDir);
321
+ if (projectDir !== await realpath(values["project-dir"] ?? dirname(root))) return;
322
+ if (typeof payload.cwd === "string") {
323
+ const rel = relative(projectDir, await realpath(payload.cwd));
324
+ if (isAbsolute2(rel) || rel === ".." || rel.startsWith(".." + sep)) return;
325
+ }
326
+ const file = values.file ?? payload.transcript_path ?? payload.session_file ?? payload.sessionFile;
327
+ const nativeId = payload.session_id ?? payload.conversation_id;
328
+ if (typeof file === "string" && file || typeof nativeId === "string")
329
+ await recordHint(directory, root, {
330
+ agent: values.agent,
331
+ ...typeof file === "string" && file ? { path: resolve(projectDir, file) } : {},
332
+ ...typeof nativeId === "string" ? { nativeId } : {},
333
+ projectDir
334
+ });
335
+ } catch {
336
+ }
337
+ }
338
+ export {
339
+ runHook
340
+ };