offwatch 0.5.12 → 0.5.14

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 (95) hide show
  1. package/README.md +132 -178
  2. package/bin/offwatch.js +6 -7
  3. package/lib/downloader.js +112 -0
  4. package/package.json +18 -11
  5. package/postinstall.js +18 -0
  6. package/src/__tests__/agent-jwt-env.test.ts +0 -79
  7. package/src/__tests__/allowed-hostname.test.ts +0 -80
  8. package/src/__tests__/auth-command-registration.test.ts +0 -16
  9. package/src/__tests__/board-auth.test.ts +0 -53
  10. package/src/__tests__/common.test.ts +0 -98
  11. package/src/__tests__/company-delete.test.ts +0 -95
  12. package/src/__tests__/company-import-export-e2e.test.ts +0 -502
  13. package/src/__tests__/company-import-url.test.ts +0 -74
  14. package/src/__tests__/company-import-zip.test.ts +0 -44
  15. package/src/__tests__/company.test.ts +0 -599
  16. package/src/__tests__/context.test.ts +0 -70
  17. package/src/__tests__/data-dir.test.ts +0 -79
  18. package/src/__tests__/doctor.test.ts +0 -102
  19. package/src/__tests__/feedback.test.ts +0 -177
  20. package/src/__tests__/helpers/embedded-postgres.ts +0 -6
  21. package/src/__tests__/helpers/zip.ts +0 -87
  22. package/src/__tests__/home-paths.test.ts +0 -44
  23. package/src/__tests__/http.test.ts +0 -106
  24. package/src/__tests__/network-bind.test.ts +0 -62
  25. package/src/__tests__/onboard.test.ts +0 -166
  26. package/src/__tests__/routines.test.ts +0 -249
  27. package/src/__tests__/telemetry.test.ts +0 -117
  28. package/src/__tests__/worktree-merge-history.test.ts +0 -492
  29. package/src/__tests__/worktree.test.ts +0 -982
  30. package/src/adapters/http/format-event.ts +0 -4
  31. package/src/adapters/http/index.ts +0 -7
  32. package/src/adapters/index.ts +0 -2
  33. package/src/adapters/process/format-event.ts +0 -4
  34. package/src/adapters/process/index.ts +0 -7
  35. package/src/adapters/registry.ts +0 -63
  36. package/src/checks/agent-jwt-secret-check.ts +0 -40
  37. package/src/checks/config-check.ts +0 -33
  38. package/src/checks/database-check.ts +0 -59
  39. package/src/checks/deployment-auth-check.ts +0 -88
  40. package/src/checks/index.ts +0 -18
  41. package/src/checks/llm-check.ts +0 -82
  42. package/src/checks/log-check.ts +0 -30
  43. package/src/checks/path-resolver.ts +0 -1
  44. package/src/checks/port-check.ts +0 -24
  45. package/src/checks/secrets-check.ts +0 -146
  46. package/src/checks/storage-check.ts +0 -51
  47. package/src/client/board-auth.ts +0 -282
  48. package/src/client/command-label.ts +0 -4
  49. package/src/client/context.ts +0 -175
  50. package/src/client/http.ts +0 -255
  51. package/src/commands/allowed-hostname.ts +0 -40
  52. package/src/commands/auth-bootstrap-ceo.ts +0 -138
  53. package/src/commands/client/activity.ts +0 -71
  54. package/src/commands/client/agent.ts +0 -315
  55. package/src/commands/client/approval.ts +0 -259
  56. package/src/commands/client/auth.ts +0 -113
  57. package/src/commands/client/common.ts +0 -221
  58. package/src/commands/client/company.ts +0 -1578
  59. package/src/commands/client/context.ts +0 -125
  60. package/src/commands/client/dashboard.ts +0 -34
  61. package/src/commands/client/feedback.ts +0 -645
  62. package/src/commands/client/issue.ts +0 -411
  63. package/src/commands/client/plugin.ts +0 -374
  64. package/src/commands/client/zip.ts +0 -129
  65. package/src/commands/configure.ts +0 -201
  66. package/src/commands/db-backup.ts +0 -102
  67. package/src/commands/doctor.ts +0 -203
  68. package/src/commands/env.ts +0 -411
  69. package/src/commands/heartbeat-run.ts +0 -344
  70. package/src/commands/onboard.ts +0 -692
  71. package/src/commands/routines.ts +0 -352
  72. package/src/commands/run.ts +0 -216
  73. package/src/commands/worktree-lib.ts +0 -279
  74. package/src/commands/worktree-merge-history-lib.ts +0 -764
  75. package/src/commands/worktree.ts +0 -2876
  76. package/src/config/data-dir.ts +0 -48
  77. package/src/config/env.ts +0 -125
  78. package/src/config/home.ts +0 -80
  79. package/src/config/hostnames.ts +0 -26
  80. package/src/config/schema.ts +0 -30
  81. package/src/config/secrets-key.ts +0 -48
  82. package/src/config/server-bind.ts +0 -183
  83. package/src/config/store.ts +0 -120
  84. package/src/index.ts +0 -182
  85. package/src/prompts/database.ts +0 -157
  86. package/src/prompts/llm.ts +0 -43
  87. package/src/prompts/logging.ts +0 -37
  88. package/src/prompts/secrets.ts +0 -99
  89. package/src/prompts/server.ts +0 -221
  90. package/src/prompts/storage.ts +0 -146
  91. package/src/telemetry.ts +0 -49
  92. package/src/utils/banner.ts +0 -24
  93. package/src/utils/net.ts +0 -18
  94. package/src/utils/path-resolver.ts +0 -25
  95. package/src/version.ts +0 -10
@@ -1,502 +0,0 @@
1
- import { execFile, spawn } from "node:child_process";
2
- import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
- import net from "node:net";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import { fileURLToPath } from "node:url";
7
- import { promisify } from "node:util";
8
- import { afterAll, beforeAll, describe, expect, it } from "vitest";
9
- import {
10
- getEmbeddedPostgresTestSupport,
11
- startEmbeddedPostgresTestDatabase,
12
- } from "./helpers/embedded-postgres.js";
13
- import { createStoredZipArchive } from "./helpers/zip.js";
14
-
15
- const execFileAsync = promisify(execFile);
16
- type ServerProcess = ReturnType<typeof spawn>;
17
-
18
- async function getAvailablePort(): Promise<number> {
19
- return await new Promise((resolve, reject) => {
20
- const server = net.createServer();
21
- server.unref();
22
- server.on("error", reject);
23
- server.listen(0, "127.0.0.1", () => {
24
- const address = server.address();
25
- if (!address || typeof address === "string") {
26
- server.close(() => reject(new Error("Failed to allocate test port")));
27
- return;
28
- }
29
- const { port } = address;
30
- server.close((error) => {
31
- if (error) reject(error);
32
- else resolve(port);
33
- });
34
- });
35
- });
36
- }
37
-
38
- const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
39
- const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
40
-
41
- if (!embeddedPostgresSupport.supported) {
42
- console.warn(
43
- `Skipping embedded Postgres company import/export e2e tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
44
- );
45
- }
46
-
47
- function writeTestConfig(configPath: string, tempRoot: string, port: number, connectionString: string) {
48
- const config = {
49
- $meta: {
50
- version: 1,
51
- updatedAt: new Date().toISOString(),
52
- source: "doctor",
53
- },
54
- database: {
55
- mode: "postgres",
56
- connectionString,
57
- embeddedPostgresDataDir: path.join(tempRoot, "embedded-db"),
58
- embeddedPostgresPort: 54329,
59
- backup: {
60
- enabled: false,
61
- intervalMinutes: 60,
62
- retentionDays: 30,
63
- dir: path.join(tempRoot, "backups"),
64
- },
65
- },
66
- logging: {
67
- mode: "file",
68
- logDir: path.join(tempRoot, "logs"),
69
- },
70
- server: {
71
- deploymentMode: "local_trusted",
72
- exposure: "private",
73
- host: "127.0.0.1",
74
- port,
75
- allowedHostnames: [],
76
- serveUi: false,
77
- },
78
- auth: {
79
- baseUrlMode: "auto",
80
- disableSignUp: false,
81
- },
82
- storage: {
83
- provider: "local_disk",
84
- localDisk: {
85
- baseDir: path.join(tempRoot, "storage"),
86
- },
87
- s3: {
88
- bucket: "paperclip",
89
- region: "us-east-1",
90
- prefix: "",
91
- forcePathStyle: false,
92
- },
93
- },
94
- secrets: {
95
- provider: "local_encrypted",
96
- strictMode: false,
97
- localEncrypted: {
98
- keyFilePath: path.join(tempRoot, "secrets", "master.key"),
99
- },
100
- },
101
- };
102
-
103
- mkdirSync(path.dirname(configPath), { recursive: true });
104
- writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
105
- }
106
-
107
- function createServerEnv(configPath: string, port: number, connectionString: string) {
108
- const env = { ...process.env };
109
- for (const key of Object.keys(env)) {
110
- if (key.startsWith("PAPERCLIP_")) {
111
- delete env[key];
112
- }
113
- }
114
- delete env.DATABASE_URL;
115
- delete env.PORT;
116
- delete env.HOST;
117
- delete env.SERVE_UI;
118
- delete env.HEARTBEAT_SCHEDULER_ENABLED;
119
-
120
- env.PAPERCLIP_CONFIG = configPath;
121
- env.DATABASE_URL = connectionString;
122
- env.HOST = "127.0.0.1";
123
- env.PORT = String(port);
124
- env.SERVE_UI = "false";
125
- env.PAPERCLIP_DB_BACKUP_ENABLED = "false";
126
- env.HEARTBEAT_SCHEDULER_ENABLED = "false";
127
- env.PAPERCLIP_MIGRATION_AUTO_APPLY = "true";
128
- env.PAPERCLIP_UI_DEV_MIDDLEWARE = "false";
129
-
130
- return env;
131
- }
132
-
133
- function createCliEnv() {
134
- const env = { ...process.env };
135
- for (const key of Object.keys(env)) {
136
- if (key.startsWith("PAPERCLIP_")) {
137
- delete env[key];
138
- }
139
- }
140
- delete env.DATABASE_URL;
141
- delete env.PORT;
142
- delete env.HOST;
143
- delete env.SERVE_UI;
144
- delete env.PAPERCLIP_DB_BACKUP_ENABLED;
145
- delete env.HEARTBEAT_SCHEDULER_ENABLED;
146
- delete env.PAPERCLIP_MIGRATION_AUTO_APPLY;
147
- delete env.PAPERCLIP_UI_DEV_MIDDLEWARE;
148
- return env;
149
- }
150
-
151
- function collectTextFiles(root: string, current: string, files: Record<string, string>) {
152
- for (const entry of readdirSync(current, { withFileTypes: true })) {
153
- const absolutePath = path.join(current, entry.name);
154
- if (entry.isDirectory()) {
155
- collectTextFiles(root, absolutePath, files);
156
- continue;
157
- }
158
- if (!entry.isFile()) continue;
159
- const relativePath = path.relative(root, absolutePath).replace(/\\/g, "/");
160
- files[relativePath] = readFileSync(absolutePath, "utf8");
161
- }
162
- }
163
-
164
- async function stopServerProcess(child: ServerProcess | null) {
165
- if (!child || child.exitCode !== null) return;
166
- child.kill("SIGTERM");
167
- await new Promise<void>((resolve) => {
168
- child.once("exit", () => resolve());
169
- setTimeout(() => {
170
- if (child.exitCode === null) {
171
- child.kill("SIGKILL");
172
- }
173
- }, 5_000);
174
- });
175
- }
176
-
177
- async function api<T>(baseUrl: string, pathname: string, init?: RequestInit): Promise<T> {
178
- const res = await fetch(`${baseUrl}${pathname}`, init);
179
- const text = await res.text();
180
- if (!res.ok) {
181
- throw new Error(`Request failed ${res.status} ${pathname}: ${text}`);
182
- }
183
- return text ? JSON.parse(text) as T : (null as T);
184
- }
185
-
186
- async function runCliJson<T>(args: string[], opts: { apiBase: string; configPath: string }) {
187
- const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
188
- const result = await execFileAsync(
189
- "pnpm",
190
- ["--silent", "paperclipai", ...args, "--api-base", opts.apiBase, "--config", opts.configPath, "--json"],
191
- {
192
- cwd: repoRoot,
193
- env: createCliEnv(),
194
- maxBuffer: 10 * 1024 * 1024,
195
- },
196
- );
197
- const stdout = result.stdout.trim();
198
- const jsonStart = stdout.search(/[\[{]/);
199
- if (jsonStart === -1) {
200
- throw new Error(`CLI did not emit JSON.\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
201
- }
202
- return JSON.parse(stdout.slice(jsonStart)) as T;
203
- }
204
-
205
- async function waitForServer(
206
- apiBase: string,
207
- child: ServerProcess,
208
- output: { stdout: string[]; stderr: string[] },
209
- ) {
210
- const startedAt = Date.now();
211
- while (Date.now() - startedAt < 30_000) {
212
- if (child.exitCode !== null) {
213
- throw new Error(
214
- `paperclipai run exited before healthcheck succeeded.\nstdout:\n${output.stdout.join("")}\nstderr:\n${output.stderr.join("")}`,
215
- );
216
- }
217
-
218
- try {
219
- const res = await fetch(`${apiBase}/api/health`);
220
- if (res.ok) return;
221
- } catch {
222
- // Server is still starting.
223
- }
224
-
225
- await new Promise((resolve) => setTimeout(resolve, 250));
226
- }
227
-
228
- throw new Error(
229
- `Timed out waiting for ${apiBase}/api/health.\nstdout:\n${output.stdout.join("")}\nstderr:\n${output.stderr.join("")}`,
230
- );
231
- }
232
-
233
- describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
234
- let tempRoot = "";
235
- let configPath = "";
236
- let exportDir = "";
237
- let apiBase = "";
238
- let serverProcess: ServerProcess | null = null;
239
- let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
240
-
241
- beforeAll(async () => {
242
- tempRoot = mkdtempSync(path.join(os.tmpdir(), "paperclip-company-cli-e2e-"));
243
- configPath = path.join(tempRoot, "config", "config.json");
244
- exportDir = path.join(tempRoot, "exported-company");
245
-
246
- tempDb = await startEmbeddedPostgresTestDatabase("paperclip-company-cli-db-");
247
-
248
- const port = await getAvailablePort();
249
- writeTestConfig(configPath, tempRoot, port, tempDb.connectionString);
250
- apiBase = `http://127.0.0.1:${port}`;
251
-
252
- const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
253
- const output = { stdout: [] as string[], stderr: [] as string[] };
254
- const child = spawn(
255
- "pnpm",
256
- ["paperclipai", "run", "--config", configPath],
257
- {
258
- cwd: repoRoot,
259
- env: createServerEnv(configPath, port, tempDb.connectionString),
260
- stdio: ["ignore", "pipe", "pipe"],
261
- },
262
- );
263
- serverProcess = child;
264
- child.stdout?.on("data", (chunk) => {
265
- output.stdout.push(String(chunk));
266
- });
267
- child.stderr?.on("data", (chunk) => {
268
- output.stderr.push(String(chunk));
269
- });
270
-
271
- await waitForServer(apiBase, child, output);
272
- }, 60_000);
273
-
274
- afterAll(async () => {
275
- await stopServerProcess(serverProcess);
276
- await tempDb?.cleanup();
277
- if (tempRoot) {
278
- rmSync(tempRoot, { recursive: true, force: true });
279
- }
280
- });
281
-
282
- it("exports a company package and imports it into new and existing companies", async () => {
283
- expect(serverProcess).not.toBeNull();
284
-
285
- const sourceCompany = await api<{ id: string; name: string; issuePrefix: string }>(apiBase, "/api/companies", {
286
- method: "POST",
287
- headers: { "content-type": "application/json" },
288
- body: JSON.stringify({ name: `CLI Export Source ${Date.now()}` }),
289
- });
290
-
291
- const sourceAgent = await api<{ id: string; name: string }>(
292
- apiBase,
293
- `/api/companies/${sourceCompany.id}/agents`,
294
- {
295
- method: "POST",
296
- headers: { "content-type": "application/json" },
297
- body: JSON.stringify({
298
- name: "Export Engineer",
299
- role: "engineer",
300
- adapterType: "claude_local",
301
- adapterConfig: {
302
- promptTemplate: "You verify company portability.",
303
- },
304
- }),
305
- },
306
- );
307
-
308
- const sourceProject = await api<{ id: string; name: string }>(
309
- apiBase,
310
- `/api/companies/${sourceCompany.id}/projects`,
311
- {
312
- method: "POST",
313
- headers: { "content-type": "application/json" },
314
- body: JSON.stringify({
315
- name: "Portability Verification",
316
- status: "in_progress",
317
- }),
318
- },
319
- );
320
-
321
- const largeIssueDescription = `Round-trip the company package through the CLI.\n\n${"portable-data ".repeat(12_000)}`;
322
-
323
- const sourceIssue = await api<{ id: string; title: string; identifier: string }>(
324
- apiBase,
325
- `/api/companies/${sourceCompany.id}/issues`,
326
- {
327
- method: "POST",
328
- headers: { "content-type": "application/json" },
329
- body: JSON.stringify({
330
- title: "Validate company import/export",
331
- description: largeIssueDescription,
332
- status: "todo",
333
- projectId: sourceProject.id,
334
- assigneeAgentId: sourceAgent.id,
335
- }),
336
- },
337
- );
338
-
339
- const exportResult = await runCliJson<{
340
- ok: boolean;
341
- out: string;
342
- filesWritten: number;
343
- }>(
344
- [
345
- "company",
346
- "export",
347
- sourceCompany.id,
348
- "--out",
349
- exportDir,
350
- "--include",
351
- "company,agents,projects,issues",
352
- ],
353
- { apiBase, configPath },
354
- );
355
-
356
- expect(exportResult.ok).toBe(true);
357
- expect(exportResult.filesWritten).toBeGreaterThan(0);
358
- expect(readFileSync(path.join(exportDir, "COMPANY.md"), "utf8")).toContain(sourceCompany.name);
359
- expect(readFileSync(path.join(exportDir, ".paperclip.yaml"), "utf8")).toContain('schema: "paperclip/v1"');
360
-
361
- const importedNew = await runCliJson<{
362
- company: { id: string; name: string; action: string };
363
- agents: Array<{ id: string | null; action: string; name: string }>;
364
- }>(
365
- [
366
- "company",
367
- "import",
368
- exportDir,
369
- "--target",
370
- "new",
371
- "--new-company-name",
372
- `Imported ${sourceCompany.name}`,
373
- "--include",
374
- "company,agents,projects,issues",
375
- "--yes",
376
- ],
377
- { apiBase, configPath },
378
- );
379
-
380
- expect(importedNew.company.action).toBe("created");
381
- expect(importedNew.agents).toHaveLength(1);
382
- expect(importedNew.agents[0]?.action).toBe("created");
383
-
384
- const importedAgents = await api<Array<{ id: string; name: string }>>(
385
- apiBase,
386
- `/api/companies/${importedNew.company.id}/agents`,
387
- );
388
- const importedProjects = await api<Array<{ id: string; name: string }>>(
389
- apiBase,
390
- `/api/companies/${importedNew.company.id}/projects`,
391
- );
392
- const importedIssues = await api<Array<{ id: string; title: string; identifier: string }>>(
393
- apiBase,
394
- `/api/companies/${importedNew.company.id}/issues`,
395
- );
396
-
397
- expect(importedAgents.map((agent) => agent.name)).toContain(sourceAgent.name);
398
- expect(importedProjects.map((project) => project.name)).toContain(sourceProject.name);
399
- expect(importedIssues.map((issue) => issue.title)).toContain(sourceIssue.title);
400
-
401
- const previewExisting = await runCliJson<{
402
- errors: string[];
403
- plan: {
404
- companyAction: string;
405
- agentPlans: Array<{ action: string }>;
406
- projectPlans: Array<{ action: string }>;
407
- issuePlans: Array<{ action: string }>;
408
- };
409
- }>(
410
- [
411
- "company",
412
- "import",
413
- exportDir,
414
- "--target",
415
- "existing",
416
- "--company-id",
417
- importedNew.company.id,
418
- "--include",
419
- "company,agents,projects,issues",
420
- "--collision",
421
- "rename",
422
- "--dry-run",
423
- ],
424
- { apiBase, configPath },
425
- );
426
-
427
- expect(previewExisting.errors).toEqual([]);
428
- expect(previewExisting.plan.companyAction).toBe("none");
429
- expect(previewExisting.plan.agentPlans.some((plan) => plan.action === "create")).toBe(true);
430
- expect(previewExisting.plan.projectPlans.some((plan) => plan.action === "create")).toBe(true);
431
- expect(previewExisting.plan.issuePlans.some((plan) => plan.action === "create")).toBe(true);
432
-
433
- const importedExisting = await runCliJson<{
434
- company: { id: string; action: string };
435
- agents: Array<{ id: string | null; action: string; name: string }>;
436
- }>(
437
- [
438
- "company",
439
- "import",
440
- exportDir,
441
- "--target",
442
- "existing",
443
- "--company-id",
444
- importedNew.company.id,
445
- "--include",
446
- "company,agents,projects,issues",
447
- "--collision",
448
- "rename",
449
- "--yes",
450
- ],
451
- { apiBase, configPath },
452
- );
453
-
454
- expect(importedExisting.company.action).toBe("unchanged");
455
- expect(importedExisting.agents.some((agent) => agent.action === "created")).toBe(true);
456
-
457
- const twiceImportedAgents = await api<Array<{ id: string; name: string }>>(
458
- apiBase,
459
- `/api/companies/${importedNew.company.id}/agents`,
460
- );
461
- const twiceImportedProjects = await api<Array<{ id: string; name: string }>>(
462
- apiBase,
463
- `/api/companies/${importedNew.company.id}/projects`,
464
- );
465
- const twiceImportedIssues = await api<Array<{ id: string; title: string; identifier: string }>>(
466
- apiBase,
467
- `/api/companies/${importedNew.company.id}/issues`,
468
- );
469
-
470
- expect(twiceImportedAgents).toHaveLength(2);
471
- expect(new Set(twiceImportedAgents.map((agent) => agent.name)).size).toBe(2);
472
- expect(twiceImportedProjects).toHaveLength(2);
473
- expect(twiceImportedIssues).toHaveLength(2);
474
-
475
- const zipPath = path.join(tempRoot, "exported-company.zip");
476
- const portableFiles: Record<string, string> = {};
477
- collectTextFiles(exportDir, exportDir, portableFiles);
478
- writeFileSync(zipPath, createStoredZipArchive(portableFiles, "paperclip-demo"));
479
-
480
- const importedFromZip = await runCliJson<{
481
- company: { id: string; name: string; action: string };
482
- agents: Array<{ id: string | null; action: string; name: string }>;
483
- }>(
484
- [
485
- "company",
486
- "import",
487
- zipPath,
488
- "--target",
489
- "new",
490
- "--new-company-name",
491
- `Zip Imported ${sourceCompany.name}`,
492
- "--include",
493
- "company,agents,projects,issues",
494
- "--yes",
495
- ],
496
- { apiBase, configPath },
497
- );
498
-
499
- expect(importedFromZip.company.action).toBe("created");
500
- expect(importedFromZip.agents.some((agent) => agent.action === "created")).toBe(true);
501
- }, 60_000);
502
- });
@@ -1,74 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- isGithubShorthand,
4
- looksLikeRepoUrl,
5
- isHttpUrl,
6
- normalizeGithubImportSource,
7
- } from "../commands/client/company.js";
8
-
9
- describe("isHttpUrl", () => {
10
- it("matches http URLs", () => {
11
- expect(isHttpUrl("http://example.com/foo")).toBe(true);
12
- });
13
-
14
- it("matches https URLs", () => {
15
- expect(isHttpUrl("https://example.com/foo")).toBe(true);
16
- });
17
-
18
- it("rejects local paths", () => {
19
- expect(isHttpUrl("/tmp/my-company")).toBe(false);
20
- expect(isHttpUrl("./relative")).toBe(false);
21
- });
22
- });
23
-
24
- describe("looksLikeRepoUrl", () => {
25
- it("matches GitHub URLs", () => {
26
- expect(looksLikeRepoUrl("https://github.com/org/repo")).toBe(true);
27
- });
28
-
29
- it("rejects URLs without owner/repo path", () => {
30
- expect(looksLikeRepoUrl("https://example.com/foo")).toBe(false);
31
- });
32
-
33
- it("rejects local paths", () => {
34
- expect(looksLikeRepoUrl("/tmp/my-company")).toBe(false);
35
- });
36
- });
37
-
38
- describe("isGithubShorthand", () => {
39
- it("matches owner/repo/path shorthands", () => {
40
- expect(isGithubShorthand("paperclipai/companies/gstack")).toBe(true);
41
- expect(isGithubShorthand("paperclipai/companies")).toBe(true);
42
- });
43
-
44
- it("rejects local-looking paths", () => {
45
- expect(isGithubShorthand("./exports/acme")).toBe(false);
46
- expect(isGithubShorthand("/tmp/acme")).toBe(false);
47
- expect(isGithubShorthand("C:\\temp\\acme")).toBe(false);
48
- });
49
- });
50
-
51
- describe("normalizeGithubImportSource", () => {
52
- it("normalizes shorthand imports to canonical GitHub sources", () => {
53
- expect(normalizeGithubImportSource("paperclipai/companies/gstack")).toBe(
54
- "https://github.com/paperclipai/companies?ref=main&path=gstack",
55
- );
56
- });
57
-
58
- it("applies --ref to shorthand imports", () => {
59
- expect(normalizeGithubImportSource("paperclipai/companies/gstack", "feature/demo")).toBe(
60
- "https://github.com/paperclipai/companies?ref=feature%2Fdemo&path=gstack",
61
- );
62
- });
63
-
64
- it("applies --ref to existing GitHub tree URLs without losing the package path", () => {
65
- expect(
66
- normalizeGithubImportSource(
67
- "https://github.com/paperclipai/companies/tree/main/gstack",
68
- "release/2026-03-23",
69
- ),
70
- ).toBe(
71
- "https://github.com/paperclipai/companies?ref=release%2F2026-03-23&path=gstack",
72
- );
73
- });
74
- });
@@ -1,44 +0,0 @@
1
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { afterEach, describe, expect, it } from "vitest";
5
- import { resolveInlineSourceFromPath } from "../commands/client/company.js";
6
- import { createStoredZipArchive } from "./helpers/zip.js";
7
-
8
- const tempDirs: string[] = [];
9
-
10
- afterEach(async () => {
11
- for (const dir of tempDirs.splice(0)) {
12
- await rm(dir, { recursive: true, force: true });
13
- }
14
- });
15
-
16
- describe("resolveInlineSourceFromPath", () => {
17
- it("imports portable files from a zip archive instead of scanning the parent directory", async () => {
18
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-company-import-zip-"));
19
- tempDirs.push(tempDir);
20
-
21
- const archivePath = path.join(tempDir, "paperclip-demo.zip");
22
- const archive = createStoredZipArchive(
23
- {
24
- "COMPANY.md": "# Company\n",
25
- ".paperclip.yaml": "schema: paperclip/v1\n",
26
- "agents/ceo/AGENT.md": "# CEO\n",
27
- "notes/todo.txt": "ignore me\n",
28
- },
29
- "paperclip-demo",
30
- );
31
- await writeFile(archivePath, archive);
32
-
33
- const resolved = await resolveInlineSourceFromPath(archivePath);
34
-
35
- expect(resolved).toEqual({
36
- rootPath: "paperclip-demo",
37
- files: {
38
- "COMPANY.md": "# Company\n",
39
- ".paperclip.yaml": "schema: paperclip/v1\n",
40
- "agents/ceo/AGENT.md": "# CEO\n",
41
- },
42
- });
43
- });
44
- });