offwatch 0.5.12 → 0.5.13
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 +132 -178
- package/bin/offwatch.js +6 -7
- package/lib/downloader.js +112 -0
- package/package.json +18 -11
- package/postinstall.js +21 -0
- package/src/__tests__/agent-jwt-env.test.ts +0 -79
- package/src/__tests__/allowed-hostname.test.ts +0 -80
- package/src/__tests__/auth-command-registration.test.ts +0 -16
- package/src/__tests__/board-auth.test.ts +0 -53
- package/src/__tests__/common.test.ts +0 -98
- package/src/__tests__/company-delete.test.ts +0 -95
- package/src/__tests__/company-import-export-e2e.test.ts +0 -502
- package/src/__tests__/company-import-url.test.ts +0 -74
- package/src/__tests__/company-import-zip.test.ts +0 -44
- package/src/__tests__/company.test.ts +0 -599
- package/src/__tests__/context.test.ts +0 -70
- package/src/__tests__/data-dir.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -102
- package/src/__tests__/feedback.test.ts +0 -177
- package/src/__tests__/helpers/embedded-postgres.ts +0 -6
- package/src/__tests__/helpers/zip.ts +0 -87
- package/src/__tests__/home-paths.test.ts +0 -44
- package/src/__tests__/http.test.ts +0 -106
- package/src/__tests__/network-bind.test.ts +0 -62
- package/src/__tests__/onboard.test.ts +0 -166
- package/src/__tests__/routines.test.ts +0 -249
- package/src/__tests__/telemetry.test.ts +0 -117
- package/src/__tests__/worktree-merge-history.test.ts +0 -492
- package/src/__tests__/worktree.test.ts +0 -982
- package/src/adapters/http/format-event.ts +0 -4
- package/src/adapters/http/index.ts +0 -7
- package/src/adapters/index.ts +0 -2
- package/src/adapters/process/format-event.ts +0 -4
- package/src/adapters/process/index.ts +0 -7
- package/src/adapters/registry.ts +0 -63
- package/src/checks/agent-jwt-secret-check.ts +0 -40
- package/src/checks/config-check.ts +0 -33
- package/src/checks/database-check.ts +0 -59
- package/src/checks/deployment-auth-check.ts +0 -88
- package/src/checks/index.ts +0 -18
- package/src/checks/llm-check.ts +0 -82
- package/src/checks/log-check.ts +0 -30
- package/src/checks/path-resolver.ts +0 -1
- package/src/checks/port-check.ts +0 -24
- package/src/checks/secrets-check.ts +0 -146
- package/src/checks/storage-check.ts +0 -51
- package/src/client/board-auth.ts +0 -282
- package/src/client/command-label.ts +0 -4
- package/src/client/context.ts +0 -175
- package/src/client/http.ts +0 -255
- package/src/commands/allowed-hostname.ts +0 -40
- package/src/commands/auth-bootstrap-ceo.ts +0 -138
- package/src/commands/client/activity.ts +0 -71
- package/src/commands/client/agent.ts +0 -315
- package/src/commands/client/approval.ts +0 -259
- package/src/commands/client/auth.ts +0 -113
- package/src/commands/client/common.ts +0 -221
- package/src/commands/client/company.ts +0 -1578
- package/src/commands/client/context.ts +0 -125
- package/src/commands/client/dashboard.ts +0 -34
- package/src/commands/client/feedback.ts +0 -645
- package/src/commands/client/issue.ts +0 -411
- package/src/commands/client/plugin.ts +0 -374
- package/src/commands/client/zip.ts +0 -129
- package/src/commands/configure.ts +0 -201
- package/src/commands/db-backup.ts +0 -102
- package/src/commands/doctor.ts +0 -203
- package/src/commands/env.ts +0 -411
- package/src/commands/heartbeat-run.ts +0 -344
- package/src/commands/onboard.ts +0 -692
- package/src/commands/routines.ts +0 -352
- package/src/commands/run.ts +0 -216
- package/src/commands/worktree-lib.ts +0 -279
- package/src/commands/worktree-merge-history-lib.ts +0 -764
- package/src/commands/worktree.ts +0 -2876
- package/src/config/data-dir.ts +0 -48
- package/src/config/env.ts +0 -125
- package/src/config/home.ts +0 -80
- package/src/config/hostnames.ts +0 -26
- package/src/config/schema.ts +0 -30
- package/src/config/secrets-key.ts +0 -48
- package/src/config/server-bind.ts +0 -183
- package/src/config/store.ts +0 -120
- package/src/index.ts +0 -182
- package/src/prompts/database.ts +0 -157
- package/src/prompts/llm.ts +0 -43
- package/src/prompts/logging.ts +0 -37
- package/src/prompts/secrets.ts +0 -99
- package/src/prompts/server.ts +0 -221
- package/src/prompts/storage.ts +0 -146
- package/src/telemetry.ts +0 -49
- package/src/utils/banner.ts +0 -24
- package/src/utils/net.ts +0 -18
- package/src/utils/path-resolver.ts +0 -25
- package/src/version.ts +0 -10
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
-
import { applyDataDirOverride } from "../config/data-dir.js";
|
|
5
|
-
|
|
6
|
-
const ORIGINAL_ENV = { ...process.env };
|
|
7
|
-
|
|
8
|
-
describe("applyDataDirOverride", () => {
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
process.env = { ...ORIGINAL_ENV };
|
|
11
|
-
delete process.env.PAPERCLIP_HOME;
|
|
12
|
-
delete process.env.PAPERCLIP_CONFIG;
|
|
13
|
-
delete process.env.PAPERCLIP_CONTEXT;
|
|
14
|
-
delete process.env.PAPERCLIP_INSTANCE_ID;
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
process.env = { ...ORIGINAL_ENV };
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("sets PAPERCLIP_HOME and isolated default config/context paths", () => {
|
|
22
|
-
const home = applyDataDirOverride({
|
|
23
|
-
dataDir: "~/paperclip-data",
|
|
24
|
-
config: undefined,
|
|
25
|
-
context: undefined,
|
|
26
|
-
}, { hasConfigOption: true, hasContextOption: true });
|
|
27
|
-
|
|
28
|
-
const expectedHome = path.resolve(os.homedir(), "paperclip-data");
|
|
29
|
-
expect(home).toBe(expectedHome);
|
|
30
|
-
expect(process.env.PAPERCLIP_HOME).toBe(expectedHome);
|
|
31
|
-
expect(process.env.PAPERCLIP_CONFIG).toBe(
|
|
32
|
-
path.resolve(expectedHome, "instances", "default", "config.json"),
|
|
33
|
-
);
|
|
34
|
-
expect(process.env.PAPERCLIP_CONTEXT).toBe(path.resolve(expectedHome, "context.json"));
|
|
35
|
-
expect(process.env.PAPERCLIP_INSTANCE_ID).toBe("default");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("uses the provided instance id when deriving default config path", () => {
|
|
39
|
-
const home = applyDataDirOverride({
|
|
40
|
-
dataDir: "/tmp/paperclip-alt",
|
|
41
|
-
instance: "dev_1",
|
|
42
|
-
config: undefined,
|
|
43
|
-
context: undefined,
|
|
44
|
-
}, { hasConfigOption: true, hasContextOption: true });
|
|
45
|
-
|
|
46
|
-
expect(home).toBe(path.resolve("/tmp/paperclip-alt"));
|
|
47
|
-
expect(process.env.PAPERCLIP_INSTANCE_ID).toBe("dev_1");
|
|
48
|
-
expect(process.env.PAPERCLIP_CONFIG).toBe(
|
|
49
|
-
path.resolve("/tmp/paperclip-alt", "instances", "dev_1", "config.json"),
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("does not override explicit config/context settings", () => {
|
|
54
|
-
process.env.PAPERCLIP_CONFIG = "/env/config.json";
|
|
55
|
-
process.env.PAPERCLIP_CONTEXT = "/env/context.json";
|
|
56
|
-
|
|
57
|
-
applyDataDirOverride({
|
|
58
|
-
dataDir: "/tmp/paperclip-alt",
|
|
59
|
-
config: "/flag/config.json",
|
|
60
|
-
context: "/flag/context.json",
|
|
61
|
-
}, { hasConfigOption: true, hasContextOption: true });
|
|
62
|
-
|
|
63
|
-
expect(process.env.PAPERCLIP_CONFIG).toBe("/env/config.json");
|
|
64
|
-
expect(process.env.PAPERCLIP_CONTEXT).toBe("/env/context.json");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("only applies defaults for options supported by the command", () => {
|
|
68
|
-
applyDataDirOverride(
|
|
69
|
-
{
|
|
70
|
-
dataDir: "/tmp/paperclip-alt",
|
|
71
|
-
},
|
|
72
|
-
{ hasConfigOption: false, hasContextOption: false },
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
expect(process.env.PAPERCLIP_HOME).toBe(path.resolve("/tmp/paperclip-alt"));
|
|
76
|
-
expect(process.env.PAPERCLIP_CONFIG).toBeUndefined();
|
|
77
|
-
expect(process.env.PAPERCLIP_CONTEXT).toBeUndefined();
|
|
78
|
-
});
|
|
79
|
-
});
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { doctor } from "../commands/doctor.js";
|
|
6
|
-
import { writeConfig } from "../config/store.js";
|
|
7
|
-
import type { PaperclipConfig } from "../config/schema.js";
|
|
8
|
-
|
|
9
|
-
const ORIGINAL_ENV = { ...process.env };
|
|
10
|
-
|
|
11
|
-
function createTempConfig(): string {
|
|
12
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-doctor-"));
|
|
13
|
-
const configPath = path.join(root, ".paperclip", "config.json");
|
|
14
|
-
const runtimeRoot = path.join(root, "runtime");
|
|
15
|
-
|
|
16
|
-
const config: PaperclipConfig = {
|
|
17
|
-
$meta: {
|
|
18
|
-
version: 1,
|
|
19
|
-
updatedAt: "2026-03-10T00:00:00.000Z",
|
|
20
|
-
source: "configure",
|
|
21
|
-
},
|
|
22
|
-
database: {
|
|
23
|
-
mode: "embedded-postgres",
|
|
24
|
-
embeddedPostgresDataDir: path.join(runtimeRoot, "db"),
|
|
25
|
-
embeddedPostgresPort: 55432,
|
|
26
|
-
backup: {
|
|
27
|
-
enabled: true,
|
|
28
|
-
intervalMinutes: 60,
|
|
29
|
-
retentionDays: 30,
|
|
30
|
-
dir: path.join(runtimeRoot, "backups"),
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
logging: {
|
|
34
|
-
mode: "file",
|
|
35
|
-
logDir: path.join(runtimeRoot, "logs"),
|
|
36
|
-
},
|
|
37
|
-
server: {
|
|
38
|
-
deploymentMode: "local_trusted",
|
|
39
|
-
exposure: "private",
|
|
40
|
-
host: "127.0.0.1",
|
|
41
|
-
port: 3199,
|
|
42
|
-
allowedHostnames: [],
|
|
43
|
-
serveUi: true,
|
|
44
|
-
},
|
|
45
|
-
auth: {
|
|
46
|
-
baseUrlMode: "auto",
|
|
47
|
-
disableSignUp: false,
|
|
48
|
-
},
|
|
49
|
-
telemetry: {
|
|
50
|
-
enabled: true,
|
|
51
|
-
},
|
|
52
|
-
storage: {
|
|
53
|
-
provider: "local_disk",
|
|
54
|
-
localDisk: {
|
|
55
|
-
baseDir: path.join(runtimeRoot, "storage"),
|
|
56
|
-
},
|
|
57
|
-
s3: {
|
|
58
|
-
bucket: "paperclip",
|
|
59
|
-
region: "us-east-1",
|
|
60
|
-
prefix: "",
|
|
61
|
-
forcePathStyle: false,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
secrets: {
|
|
65
|
-
provider: "local_encrypted",
|
|
66
|
-
strictMode: false,
|
|
67
|
-
localEncrypted: {
|
|
68
|
-
keyFilePath: path.join(runtimeRoot, "secrets", "master.key"),
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
writeConfig(config, configPath);
|
|
74
|
-
return configPath;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
describe("doctor", () => {
|
|
78
|
-
beforeEach(() => {
|
|
79
|
-
process.env = { ...ORIGINAL_ENV };
|
|
80
|
-
delete process.env.PAPERCLIP_AGENT_JWT_SECRET;
|
|
81
|
-
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY;
|
|
82
|
-
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
afterEach(() => {
|
|
86
|
-
process.env = { ...ORIGINAL_ENV };
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("re-runs repairable checks so repaired failures do not remain blocking", async () => {
|
|
90
|
-
const configPath = createTempConfig();
|
|
91
|
-
|
|
92
|
-
const summary = await doctor({
|
|
93
|
-
config: configPath,
|
|
94
|
-
repair: true,
|
|
95
|
-
yes: true,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
expect(summary.failed).toBe(0);
|
|
99
|
-
expect(summary.warned).toBe(0);
|
|
100
|
-
expect(process.env.PAPERCLIP_AGENT_JWT_SECRET).toBeTruthy();
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { mkdtemp, readFile } from "node:fs/promises";
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import { describe, expect, it } from "vitest";
|
|
6
|
-
import type { FeedbackTrace } from "@paperclipai/shared";
|
|
7
|
-
import { readZipArchive } from "../commands/client/zip.js";
|
|
8
|
-
import {
|
|
9
|
-
buildFeedbackTraceQuery,
|
|
10
|
-
registerFeedbackCommands,
|
|
11
|
-
renderFeedbackReport,
|
|
12
|
-
summarizeFeedbackTraces,
|
|
13
|
-
writeFeedbackExportBundle,
|
|
14
|
-
} from "../commands/client/feedback.js";
|
|
15
|
-
|
|
16
|
-
function makeTrace(overrides: Partial<FeedbackTrace> = {}): FeedbackTrace {
|
|
17
|
-
return {
|
|
18
|
-
id: "trace-12345678",
|
|
19
|
-
companyId: "company-123",
|
|
20
|
-
feedbackVoteId: "vote-12345678",
|
|
21
|
-
issueId: "issue-123",
|
|
22
|
-
projectId: "project-123",
|
|
23
|
-
issueIdentifier: "PAP-123",
|
|
24
|
-
issueTitle: "Fix the feedback command",
|
|
25
|
-
authorUserId: "user-123",
|
|
26
|
-
targetType: "issue_comment",
|
|
27
|
-
targetId: "comment-123",
|
|
28
|
-
vote: "down",
|
|
29
|
-
status: "pending",
|
|
30
|
-
destination: "paperclip_labs_feedback_v1",
|
|
31
|
-
exportId: null,
|
|
32
|
-
consentVersion: "feedback-data-sharing-v1",
|
|
33
|
-
schemaVersion: "1",
|
|
34
|
-
bundleVersion: "1",
|
|
35
|
-
payloadVersion: "1",
|
|
36
|
-
payloadDigest: null,
|
|
37
|
-
payloadSnapshot: {
|
|
38
|
-
vote: {
|
|
39
|
-
value: "down",
|
|
40
|
-
reason: "Needed more detail",
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
targetSummary: {
|
|
44
|
-
label: "Comment",
|
|
45
|
-
excerpt: "The first answer was too vague.",
|
|
46
|
-
authorAgentId: "agent-123",
|
|
47
|
-
authorUserId: null,
|
|
48
|
-
createdAt: new Date("2026-03-31T12:00:00.000Z"),
|
|
49
|
-
documentKey: null,
|
|
50
|
-
documentTitle: null,
|
|
51
|
-
revisionNumber: null,
|
|
52
|
-
},
|
|
53
|
-
redactionSummary: null,
|
|
54
|
-
attemptCount: 0,
|
|
55
|
-
lastAttemptedAt: null,
|
|
56
|
-
exportedAt: null,
|
|
57
|
-
failureReason: null,
|
|
58
|
-
createdAt: new Date("2026-03-31T12:01:00.000Z"),
|
|
59
|
-
updatedAt: new Date("2026-03-31T12:02:00.000Z"),
|
|
60
|
-
...overrides,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
describe("registerFeedbackCommands", () => {
|
|
65
|
-
it("registers the top-level feedback commands", () => {
|
|
66
|
-
const program = new Command();
|
|
67
|
-
|
|
68
|
-
expect(() => registerFeedbackCommands(program)).not.toThrow();
|
|
69
|
-
|
|
70
|
-
const feedback = program.commands.find((command) => command.name() === "feedback");
|
|
71
|
-
expect(feedback).toBeDefined();
|
|
72
|
-
expect(feedback?.commands.map((command) => command.name())).toEqual(["report", "export"]);
|
|
73
|
-
expect(feedback?.commands[0]?.options.filter((option) => option.long === "--company-id")).toHaveLength(1);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe("buildFeedbackTraceQuery", () => {
|
|
78
|
-
it("encodes all supported filters", () => {
|
|
79
|
-
expect(
|
|
80
|
-
buildFeedbackTraceQuery({
|
|
81
|
-
targetType: "issue_comment",
|
|
82
|
-
vote: "down",
|
|
83
|
-
status: "pending",
|
|
84
|
-
projectId: "project-123",
|
|
85
|
-
issueId: "issue-123",
|
|
86
|
-
from: "2026-03-31T00:00:00.000Z",
|
|
87
|
-
to: "2026-03-31T23:59:59.999Z",
|
|
88
|
-
sharedOnly: true,
|
|
89
|
-
}),
|
|
90
|
-
).toBe(
|
|
91
|
-
"?targetType=issue_comment&vote=down&status=pending&projectId=project-123&issueId=issue-123&from=2026-03-31T00%3A00%3A00.000Z&to=2026-03-31T23%3A59%3A59.999Z&sharedOnly=true&includePayload=true",
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe("renderFeedbackReport", () => {
|
|
97
|
-
it("includes summary counts and the optional reason", () => {
|
|
98
|
-
const traces = [
|
|
99
|
-
makeTrace(),
|
|
100
|
-
makeTrace({
|
|
101
|
-
id: "trace-87654321",
|
|
102
|
-
feedbackVoteId: "vote-87654321",
|
|
103
|
-
vote: "up",
|
|
104
|
-
status: "local_only",
|
|
105
|
-
payloadSnapshot: {
|
|
106
|
-
vote: {
|
|
107
|
-
value: "up",
|
|
108
|
-
reason: null,
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
}),
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
const report = renderFeedbackReport({
|
|
115
|
-
apiBase: "http://127.0.0.1:3100",
|
|
116
|
-
companyId: "company-123",
|
|
117
|
-
traces,
|
|
118
|
-
summary: summarizeFeedbackTraces(traces),
|
|
119
|
-
includePayloads: false,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
expect(report).toContain("Paperclip Feedback Report");
|
|
123
|
-
expect(report).toContain("thumbs up");
|
|
124
|
-
expect(report).toContain("thumbs down");
|
|
125
|
-
expect(report).toContain("Needed more detail");
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe("writeFeedbackExportBundle", () => {
|
|
130
|
-
it("writes votes, traces, a manifest, and a zip archive", async () => {
|
|
131
|
-
const tempDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-feedback-export-"));
|
|
132
|
-
const outputDir = path.join(tempDir, "feedback-export");
|
|
133
|
-
const traces = [
|
|
134
|
-
makeTrace(),
|
|
135
|
-
makeTrace({
|
|
136
|
-
id: "trace-abcdef12",
|
|
137
|
-
feedbackVoteId: "vote-abcdef12",
|
|
138
|
-
issueIdentifier: "PAP-124",
|
|
139
|
-
issueId: "issue-124",
|
|
140
|
-
vote: "up",
|
|
141
|
-
status: "local_only",
|
|
142
|
-
payloadSnapshot: {
|
|
143
|
-
vote: {
|
|
144
|
-
value: "up",
|
|
145
|
-
reason: null,
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
}),
|
|
149
|
-
];
|
|
150
|
-
|
|
151
|
-
const exported = await writeFeedbackExportBundle({
|
|
152
|
-
apiBase: "http://127.0.0.1:3100",
|
|
153
|
-
companyId: "company-123",
|
|
154
|
-
traces,
|
|
155
|
-
outputDir,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
expect(exported.manifest.summary.total).toBe(2);
|
|
159
|
-
expect(exported.manifest.summary.withReason).toBe(1);
|
|
160
|
-
|
|
161
|
-
const manifest = JSON.parse(await readFile(path.join(outputDir, "index.json"), "utf8")) as {
|
|
162
|
-
files: { votes: string[]; traces: string[]; zip: string };
|
|
163
|
-
};
|
|
164
|
-
expect(manifest.files.votes).toHaveLength(2);
|
|
165
|
-
expect(manifest.files.traces).toHaveLength(2);
|
|
166
|
-
|
|
167
|
-
const archive = await readFile(exported.zipPath);
|
|
168
|
-
const zip = await readZipArchive(archive);
|
|
169
|
-
expect(Object.keys(zip.files)).toEqual(
|
|
170
|
-
expect.arrayContaining([
|
|
171
|
-
"index.json",
|
|
172
|
-
`votes/${manifest.files.votes[0]}`,
|
|
173
|
-
`traces/${manifest.files.traces[0]}`,
|
|
174
|
-
]),
|
|
175
|
-
);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
function writeUint16(target: Uint8Array, offset: number, value: number) {
|
|
2
|
-
target[offset] = value & 0xff;
|
|
3
|
-
target[offset + 1] = (value >>> 8) & 0xff;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
function writeUint32(target: Uint8Array, offset: number, value: number) {
|
|
7
|
-
target[offset] = value & 0xff;
|
|
8
|
-
target[offset + 1] = (value >>> 8) & 0xff;
|
|
9
|
-
target[offset + 2] = (value >>> 16) & 0xff;
|
|
10
|
-
target[offset + 3] = (value >>> 24) & 0xff;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function crc32(bytes: Uint8Array) {
|
|
14
|
-
let crc = 0xffffffff;
|
|
15
|
-
for (const byte of bytes) {
|
|
16
|
-
crc ^= byte;
|
|
17
|
-
for (let bit = 0; bit < 8; bit += 1) {
|
|
18
|
-
crc = (crc & 1) === 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return (crc ^ 0xffffffff) >>> 0;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function createStoredZipArchive(files: Record<string, string>, rootPath: string) {
|
|
25
|
-
const encoder = new TextEncoder();
|
|
26
|
-
const localChunks: Uint8Array[] = [];
|
|
27
|
-
const centralChunks: Uint8Array[] = [];
|
|
28
|
-
let localOffset = 0;
|
|
29
|
-
let entryCount = 0;
|
|
30
|
-
|
|
31
|
-
for (const [relativePath, content] of Object.entries(files).sort(([left], [right]) => left.localeCompare(right))) {
|
|
32
|
-
const fileName = encoder.encode(`${rootPath}/${relativePath}`);
|
|
33
|
-
const body = encoder.encode(content);
|
|
34
|
-
const checksum = crc32(body);
|
|
35
|
-
|
|
36
|
-
const localHeader = new Uint8Array(30 + fileName.length);
|
|
37
|
-
writeUint32(localHeader, 0, 0x04034b50);
|
|
38
|
-
writeUint16(localHeader, 4, 20);
|
|
39
|
-
writeUint16(localHeader, 6, 0x0800);
|
|
40
|
-
writeUint16(localHeader, 8, 0);
|
|
41
|
-
writeUint32(localHeader, 14, checksum);
|
|
42
|
-
writeUint32(localHeader, 18, body.length);
|
|
43
|
-
writeUint32(localHeader, 22, body.length);
|
|
44
|
-
writeUint16(localHeader, 26, fileName.length);
|
|
45
|
-
localHeader.set(fileName, 30);
|
|
46
|
-
|
|
47
|
-
const centralHeader = new Uint8Array(46 + fileName.length);
|
|
48
|
-
writeUint32(centralHeader, 0, 0x02014b50);
|
|
49
|
-
writeUint16(centralHeader, 4, 20);
|
|
50
|
-
writeUint16(centralHeader, 6, 20);
|
|
51
|
-
writeUint16(centralHeader, 8, 0x0800);
|
|
52
|
-
writeUint16(centralHeader, 10, 0);
|
|
53
|
-
writeUint32(centralHeader, 16, checksum);
|
|
54
|
-
writeUint32(centralHeader, 20, body.length);
|
|
55
|
-
writeUint32(centralHeader, 24, body.length);
|
|
56
|
-
writeUint16(centralHeader, 28, fileName.length);
|
|
57
|
-
writeUint32(centralHeader, 42, localOffset);
|
|
58
|
-
centralHeader.set(fileName, 46);
|
|
59
|
-
|
|
60
|
-
localChunks.push(localHeader, body);
|
|
61
|
-
centralChunks.push(centralHeader);
|
|
62
|
-
localOffset += localHeader.length + body.length;
|
|
63
|
-
entryCount += 1;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const centralDirectoryLength = centralChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
67
|
-
const archive = new Uint8Array(
|
|
68
|
-
localChunks.reduce((sum, chunk) => sum + chunk.length, 0) + centralDirectoryLength + 22,
|
|
69
|
-
);
|
|
70
|
-
let offset = 0;
|
|
71
|
-
for (const chunk of localChunks) {
|
|
72
|
-
archive.set(chunk, offset);
|
|
73
|
-
offset += chunk.length;
|
|
74
|
-
}
|
|
75
|
-
const centralDirectoryOffset = offset;
|
|
76
|
-
for (const chunk of centralChunks) {
|
|
77
|
-
archive.set(chunk, offset);
|
|
78
|
-
offset += chunk.length;
|
|
79
|
-
}
|
|
80
|
-
writeUint32(archive, offset, 0x06054b50);
|
|
81
|
-
writeUint16(archive, offset + 8, entryCount);
|
|
82
|
-
writeUint16(archive, offset + 10, entryCount);
|
|
83
|
-
writeUint32(archive, offset + 12, centralDirectoryLength);
|
|
84
|
-
writeUint32(archive, offset + 16, centralDirectoryOffset);
|
|
85
|
-
|
|
86
|
-
return archive;
|
|
87
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
-
import {
|
|
5
|
-
describeLocalInstancePaths,
|
|
6
|
-
expandHomePrefix,
|
|
7
|
-
resolvePaperclipHomeDir,
|
|
8
|
-
resolvePaperclipInstanceId,
|
|
9
|
-
} from "../config/home.js";
|
|
10
|
-
|
|
11
|
-
const ORIGINAL_ENV = { ...process.env };
|
|
12
|
-
|
|
13
|
-
describe("home path resolution", () => {
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
process.env = { ...ORIGINAL_ENV };
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("defaults to ~/.paperclip and default instance", () => {
|
|
19
|
-
delete process.env.PAPERCLIP_HOME;
|
|
20
|
-
delete process.env.PAPERCLIP_INSTANCE_ID;
|
|
21
|
-
|
|
22
|
-
const paths = describeLocalInstancePaths();
|
|
23
|
-
expect(paths.homeDir).toBe(path.resolve(os.homedir(), ".paperclip"));
|
|
24
|
-
expect(paths.instanceId).toBe("default");
|
|
25
|
-
expect(paths.configPath).toBe(path.resolve(os.homedir(), ".paperclip", "instances", "default", "config.json"));
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("supports PAPERCLIP_HOME and explicit instance ids", () => {
|
|
29
|
-
process.env.PAPERCLIP_HOME = "~/paperclip-home";
|
|
30
|
-
|
|
31
|
-
const home = resolvePaperclipHomeDir();
|
|
32
|
-
expect(home).toBe(path.resolve(os.homedir(), "paperclip-home"));
|
|
33
|
-
expect(resolvePaperclipInstanceId("dev_1")).toBe("dev_1");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("rejects invalid instance ids", () => {
|
|
37
|
-
expect(() => resolvePaperclipInstanceId("bad/id")).toThrow(/Invalid instance id/);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("expands ~ prefixes", () => {
|
|
41
|
-
expect(expandHomePrefix("~")).toBe(os.homedir());
|
|
42
|
-
expect(expandHomePrefix("~/x/y")).toBe(path.resolve(os.homedir(), "x/y"));
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { ApiConnectionError, ApiRequestError, PaperclipApiClient } from "../client/http.js";
|
|
3
|
-
|
|
4
|
-
describe("PaperclipApiClient", () => {
|
|
5
|
-
afterEach(() => {
|
|
6
|
-
vi.restoreAllMocks();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it("adds authorization and run-id headers", async () => {
|
|
10
|
-
const fetchMock = vi.fn().mockResolvedValue(
|
|
11
|
-
new Response(JSON.stringify({ ok: true }), { status: 200 }),
|
|
12
|
-
);
|
|
13
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
14
|
-
|
|
15
|
-
const client = new PaperclipApiClient({
|
|
16
|
-
apiBase: "http://localhost:3100",
|
|
17
|
-
apiKey: "token-123",
|
|
18
|
-
runId: "run-abc",
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
await client.post("/api/test", { hello: "world" });
|
|
22
|
-
|
|
23
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
24
|
-
const call = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
25
|
-
expect(call[0]).toContain("/api/test");
|
|
26
|
-
|
|
27
|
-
const headers = call[1].headers as Record<string, string>;
|
|
28
|
-
expect(headers.authorization).toBe("Bearer token-123");
|
|
29
|
-
expect(headers["x-paperclip-run-id"]).toBe("run-abc");
|
|
30
|
-
expect(headers["content-type"]).toBe("application/json");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("returns null on ignoreNotFound", async () => {
|
|
34
|
-
const fetchMock = vi.fn().mockResolvedValue(
|
|
35
|
-
new Response(JSON.stringify({ error: "Not found" }), { status: 404 }),
|
|
36
|
-
);
|
|
37
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
38
|
-
|
|
39
|
-
const client = new PaperclipApiClient({ apiBase: "http://localhost:3100" });
|
|
40
|
-
const result = await client.get("/api/missing", { ignoreNotFound: true });
|
|
41
|
-
expect(result).toBeNull();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("throws ApiRequestError with details", async () => {
|
|
45
|
-
const fetchMock = vi.fn().mockResolvedValue(
|
|
46
|
-
new Response(
|
|
47
|
-
JSON.stringify({ error: "Issue checkout conflict", details: { issueId: "1" } }),
|
|
48
|
-
{ status: 409 },
|
|
49
|
-
),
|
|
50
|
-
);
|
|
51
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
52
|
-
|
|
53
|
-
const client = new PaperclipApiClient({ apiBase: "http://localhost:3100" });
|
|
54
|
-
|
|
55
|
-
await expect(client.post("/api/issues/1/checkout", {})).rejects.toMatchObject({
|
|
56
|
-
status: 409,
|
|
57
|
-
message: "Issue checkout conflict",
|
|
58
|
-
details: { issueId: "1" },
|
|
59
|
-
} satisfies Partial<ApiRequestError>);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("throws ApiConnectionError with recovery guidance when fetch fails", async () => {
|
|
63
|
-
const fetchMock = vi.fn().mockRejectedValue(new TypeError("fetch failed"));
|
|
64
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
65
|
-
|
|
66
|
-
const client = new PaperclipApiClient({ apiBase: "http://localhost:3100" });
|
|
67
|
-
|
|
68
|
-
await expect(client.post("/api/companies/import/preview", {})).rejects.toBeInstanceOf(ApiConnectionError);
|
|
69
|
-
await expect(client.post("/api/companies/import/preview", {})).rejects.toMatchObject({
|
|
70
|
-
url: "http://localhost:3100/api/companies/import/preview",
|
|
71
|
-
method: "POST",
|
|
72
|
-
causeMessage: "fetch failed",
|
|
73
|
-
} satisfies Partial<ApiConnectionError>);
|
|
74
|
-
await expect(client.post("/api/companies/import/preview", {})).rejects.toThrow(
|
|
75
|
-
/Could not reach the Paperclip API\./,
|
|
76
|
-
);
|
|
77
|
-
await expect(client.post("/api/companies/import/preview", {})).rejects.toThrow(
|
|
78
|
-
/curl http:\/\/localhost:3100\/api\/health/,
|
|
79
|
-
);
|
|
80
|
-
await expect(client.post("/api/companies/import/preview", {})).rejects.toThrow(
|
|
81
|
-
/pnpm dev|pnpm paperclipai run/,
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("retries once after interactive auth recovery", async () => {
|
|
86
|
-
const fetchMock = vi
|
|
87
|
-
.fn()
|
|
88
|
-
.mockResolvedValueOnce(new Response(JSON.stringify({ error: "Board access required" }), { status: 403 }))
|
|
89
|
-
.mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { status: 200 }));
|
|
90
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
91
|
-
|
|
92
|
-
const recoverAuth = vi.fn().mockResolvedValue("board-token-123");
|
|
93
|
-
const client = new PaperclipApiClient({
|
|
94
|
-
apiBase: "http://localhost:3100",
|
|
95
|
-
recoverAuth,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const result = await client.post<{ ok: boolean }>("/api/test", { hello: "world" });
|
|
99
|
-
|
|
100
|
-
expect(result).toEqual({ ok: true });
|
|
101
|
-
expect(recoverAuth).toHaveBeenCalledOnce();
|
|
102
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
103
|
-
const retryHeaders = fetchMock.mock.calls[1]?.[1]?.headers as Record<string, string>;
|
|
104
|
-
expect(retryHeaders.authorization).toBe("Bearer board-token-123");
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { resolveRuntimeBind, validateConfiguredBindMode } from "@paperclipai/shared";
|
|
3
|
-
import { buildPresetServerConfig } from "../config/server-bind.js";
|
|
4
|
-
|
|
5
|
-
describe("network bind helpers", () => {
|
|
6
|
-
it("rejects non-loopback bind modes in local_trusted", () => {
|
|
7
|
-
expect(
|
|
8
|
-
validateConfiguredBindMode({
|
|
9
|
-
deploymentMode: "local_trusted",
|
|
10
|
-
deploymentExposure: "private",
|
|
11
|
-
bind: "lan",
|
|
12
|
-
host: "0.0.0.0",
|
|
13
|
-
}),
|
|
14
|
-
).toContain("local_trusted requires server.bind=loopback");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("resolves tailnet bind using the detected tailscale address", () => {
|
|
18
|
-
const resolved = resolveRuntimeBind({
|
|
19
|
-
bind: "tailnet",
|
|
20
|
-
host: "127.0.0.1",
|
|
21
|
-
tailnetBindHost: "100.64.0.8",
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
expect(resolved.errors).toEqual([]);
|
|
25
|
-
expect(resolved.host).toBe("100.64.0.8");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("requires a custom bind host when bind=custom", () => {
|
|
29
|
-
const resolved = resolveRuntimeBind({
|
|
30
|
-
bind: "custom",
|
|
31
|
-
host: "127.0.0.1",
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
expect(resolved.errors).toContain("server.customBindHost is required when server.bind=custom");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("stores the detected tailscale address for tailnet presets", () => {
|
|
38
|
-
process.env.PAPERCLIP_TAILNET_BIND_HOST = "100.64.0.8";
|
|
39
|
-
|
|
40
|
-
const preset = buildPresetServerConfig("tailnet", {
|
|
41
|
-
port: 3100,
|
|
42
|
-
allowedHostnames: [],
|
|
43
|
-
serveUi: true,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
expect(preset.server.host).toBe("100.64.0.8");
|
|
47
|
-
|
|
48
|
-
delete process.env.PAPERCLIP_TAILNET_BIND_HOST;
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("falls back to loopback when no tailscale address is available for tailnet presets", () => {
|
|
52
|
-
delete process.env.PAPERCLIP_TAILNET_BIND_HOST;
|
|
53
|
-
|
|
54
|
-
const preset = buildPresetServerConfig("tailnet", {
|
|
55
|
-
port: 3100,
|
|
56
|
-
allowedHostnames: [],
|
|
57
|
-
serveUi: true,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
expect(preset.server.host).toBe("127.0.0.1");
|
|
61
|
-
});
|
|
62
|
-
});
|