offwatch 0.5.11 → 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 +17 -7
- 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,374 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import pc from "picocolors";
|
|
4
|
-
import {
|
|
5
|
-
addCommonClientOptions,
|
|
6
|
-
handleCommandError,
|
|
7
|
-
printOutput,
|
|
8
|
-
resolveCommandContext,
|
|
9
|
-
type BaseClientOptions,
|
|
10
|
-
} from "./common.js";
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Types mirroring server-side shapes
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
interface PluginRecord {
|
|
17
|
-
id: string;
|
|
18
|
-
pluginKey: string;
|
|
19
|
-
packageName: string;
|
|
20
|
-
version: string;
|
|
21
|
-
status: string;
|
|
22
|
-
displayName?: string;
|
|
23
|
-
lastError?: string | null;
|
|
24
|
-
installedAt: string;
|
|
25
|
-
updatedAt: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Option types
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
interface PluginListOptions extends BaseClientOptions {
|
|
34
|
-
status?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface PluginInstallOptions extends BaseClientOptions {
|
|
38
|
-
local?: boolean;
|
|
39
|
-
version?: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface PluginUninstallOptions extends BaseClientOptions {
|
|
43
|
-
force?: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Helpers
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Resolve a local path argument to an absolute path so the server can find the
|
|
52
|
-
* plugin on disk regardless of where the user ran the CLI.
|
|
53
|
-
*/
|
|
54
|
-
function resolvePackageArg(packageArg: string, isLocal: boolean): string {
|
|
55
|
-
if (!isLocal) return packageArg;
|
|
56
|
-
// Already absolute
|
|
57
|
-
if (path.isAbsolute(packageArg)) return packageArg;
|
|
58
|
-
// Expand leading ~ to home directory
|
|
59
|
-
if (packageArg.startsWith("~")) {
|
|
60
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
61
|
-
return path.resolve(home, packageArg.slice(1).replace(/^[\\/]/, ""));
|
|
62
|
-
}
|
|
63
|
-
return path.resolve(process.cwd(), packageArg);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function formatPlugin(p: PluginRecord): string {
|
|
67
|
-
const statusColor =
|
|
68
|
-
p.status === "ready"
|
|
69
|
-
? pc.green(p.status)
|
|
70
|
-
: p.status === "error"
|
|
71
|
-
? pc.red(p.status)
|
|
72
|
-
: p.status === "disabled"
|
|
73
|
-
? pc.dim(p.status)
|
|
74
|
-
: pc.yellow(p.status);
|
|
75
|
-
|
|
76
|
-
const parts = [
|
|
77
|
-
`key=${pc.bold(p.pluginKey)}`,
|
|
78
|
-
`status=${statusColor}`,
|
|
79
|
-
`version=${p.version}`,
|
|
80
|
-
`id=${pc.dim(p.id)}`,
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
if (p.lastError) {
|
|
84
|
-
parts.push(`error=${pc.red(p.lastError.slice(0, 80))}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return parts.join(" ");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
// Command registration
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
|
|
94
|
-
export function registerPluginCommands(program: Command): void {
|
|
95
|
-
const plugin = program.command("plugin").description("Plugin lifecycle management");
|
|
96
|
-
|
|
97
|
-
// -------------------------------------------------------------------------
|
|
98
|
-
// plugin list
|
|
99
|
-
// -------------------------------------------------------------------------
|
|
100
|
-
addCommonClientOptions(
|
|
101
|
-
plugin
|
|
102
|
-
.command("list")
|
|
103
|
-
.description("List installed plugins")
|
|
104
|
-
.option("--status <status>", "Filter by status (ready, error, disabled, installed, upgrade_pending)")
|
|
105
|
-
.action(async (opts: PluginListOptions) => {
|
|
106
|
-
try {
|
|
107
|
-
const ctx = resolveCommandContext(opts);
|
|
108
|
-
const qs = opts.status ? `?status=${encodeURIComponent(opts.status)}` : "";
|
|
109
|
-
const plugins = await ctx.api.get<PluginRecord[]>(`/api/plugins${qs}`);
|
|
110
|
-
|
|
111
|
-
if (ctx.json) {
|
|
112
|
-
printOutput(plugins, { json: true });
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const rows = plugins ?? [];
|
|
117
|
-
if (rows.length === 0) {
|
|
118
|
-
console.log(pc.dim("No plugins installed."));
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
for (const p of rows) {
|
|
123
|
-
console.log(formatPlugin(p));
|
|
124
|
-
}
|
|
125
|
-
} catch (err) {
|
|
126
|
-
handleCommandError(err);
|
|
127
|
-
}
|
|
128
|
-
}),
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
// -------------------------------------------------------------------------
|
|
132
|
-
// plugin install <package-or-path>
|
|
133
|
-
// -------------------------------------------------------------------------
|
|
134
|
-
addCommonClientOptions(
|
|
135
|
-
plugin
|
|
136
|
-
.command("install <package>")
|
|
137
|
-
.description(
|
|
138
|
-
"Install a plugin from a local path or npm package.\n" +
|
|
139
|
-
" Examples:\n" +
|
|
140
|
-
" paperclipai plugin install ./my-plugin # local path\n" +
|
|
141
|
-
" paperclipai plugin install @acme/plugin-linear # npm package\n" +
|
|
142
|
-
" paperclipai plugin install @acme/plugin-linear@1.2 # pinned version",
|
|
143
|
-
)
|
|
144
|
-
.option("-l, --local", "Treat <package> as a local filesystem path", false)
|
|
145
|
-
.option("--version <version>", "Specific npm version to install (npm packages only)")
|
|
146
|
-
.action(async (packageArg: string, opts: PluginInstallOptions) => {
|
|
147
|
-
try {
|
|
148
|
-
const ctx = resolveCommandContext(opts);
|
|
149
|
-
|
|
150
|
-
// Auto-detect local paths: starts with . or / or ~ or is an absolute path
|
|
151
|
-
const isLocal =
|
|
152
|
-
opts.local ||
|
|
153
|
-
packageArg.startsWith("./") ||
|
|
154
|
-
packageArg.startsWith("../") ||
|
|
155
|
-
packageArg.startsWith("/") ||
|
|
156
|
-
packageArg.startsWith("~");
|
|
157
|
-
|
|
158
|
-
const resolvedPackage = resolvePackageArg(packageArg, isLocal);
|
|
159
|
-
|
|
160
|
-
if (!ctx.json) {
|
|
161
|
-
console.log(
|
|
162
|
-
pc.dim(
|
|
163
|
-
isLocal
|
|
164
|
-
? `Installing plugin from local path: ${resolvedPackage}`
|
|
165
|
-
: `Installing plugin: ${resolvedPackage}${opts.version ? `@${opts.version}` : ""}`,
|
|
166
|
-
),
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const installedPlugin = await ctx.api.post<PluginRecord>("/api/plugins/install", {
|
|
171
|
-
packageName: resolvedPackage,
|
|
172
|
-
version: opts.version,
|
|
173
|
-
isLocalPath: isLocal,
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
if (ctx.json) {
|
|
177
|
-
printOutput(installedPlugin, { json: true });
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (!installedPlugin) {
|
|
182
|
-
console.log(pc.dim("Install returned no plugin record."));
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
console.log(
|
|
187
|
-
pc.green(
|
|
188
|
-
`✓ Installed ${pc.bold(installedPlugin.pluginKey)} v${installedPlugin.version} (${installedPlugin.status})`,
|
|
189
|
-
),
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
if (installedPlugin.lastError) {
|
|
193
|
-
console.log(pc.red(` Warning: ${installedPlugin.lastError}`));
|
|
194
|
-
}
|
|
195
|
-
} catch (err) {
|
|
196
|
-
handleCommandError(err);
|
|
197
|
-
}
|
|
198
|
-
}),
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
// -------------------------------------------------------------------------
|
|
202
|
-
// plugin uninstall <plugin-key-or-id>
|
|
203
|
-
// -------------------------------------------------------------------------
|
|
204
|
-
addCommonClientOptions(
|
|
205
|
-
plugin
|
|
206
|
-
.command("uninstall <pluginKey>")
|
|
207
|
-
.description(
|
|
208
|
-
"Uninstall a plugin by its plugin key or database ID.\n" +
|
|
209
|
-
" Use --force to hard-purge all state and config.",
|
|
210
|
-
)
|
|
211
|
-
.option("--force", "Purge all plugin state and config (hard delete)", false)
|
|
212
|
-
.action(async (pluginKey: string, opts: PluginUninstallOptions) => {
|
|
213
|
-
try {
|
|
214
|
-
const ctx = resolveCommandContext(opts);
|
|
215
|
-
const purge = opts.force === true;
|
|
216
|
-
const qs = purge ? "?purge=true" : "";
|
|
217
|
-
|
|
218
|
-
if (!ctx.json) {
|
|
219
|
-
console.log(
|
|
220
|
-
pc.dim(
|
|
221
|
-
purge
|
|
222
|
-
? `Uninstalling and purging plugin: ${pluginKey}`
|
|
223
|
-
: `Uninstalling plugin: ${pluginKey}`,
|
|
224
|
-
),
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const result = await ctx.api.delete<PluginRecord | null>(
|
|
229
|
-
`/api/plugins/${encodeURIComponent(pluginKey)}${qs}`,
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
if (ctx.json) {
|
|
233
|
-
printOutput(result, { json: true });
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
console.log(pc.green(`✓ Uninstalled ${pc.bold(pluginKey)}${purge ? " (purged)" : ""}`));
|
|
238
|
-
} catch (err) {
|
|
239
|
-
handleCommandError(err);
|
|
240
|
-
}
|
|
241
|
-
}),
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
// -------------------------------------------------------------------------
|
|
245
|
-
// plugin enable <plugin-key-or-id>
|
|
246
|
-
// -------------------------------------------------------------------------
|
|
247
|
-
addCommonClientOptions(
|
|
248
|
-
plugin
|
|
249
|
-
.command("enable <pluginKey>")
|
|
250
|
-
.description("Enable a disabled or errored plugin")
|
|
251
|
-
.action(async (pluginKey: string, opts: BaseClientOptions) => {
|
|
252
|
-
try {
|
|
253
|
-
const ctx = resolveCommandContext(opts);
|
|
254
|
-
const result = await ctx.api.post<PluginRecord>(
|
|
255
|
-
`/api/plugins/${encodeURIComponent(pluginKey)}/enable`,
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
if (ctx.json) {
|
|
259
|
-
printOutput(result, { json: true });
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
console.log(pc.green(`✓ Enabled ${pc.bold(pluginKey)} — status: ${result?.status ?? "unknown"}`));
|
|
264
|
-
} catch (err) {
|
|
265
|
-
handleCommandError(err);
|
|
266
|
-
}
|
|
267
|
-
}),
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
// -------------------------------------------------------------------------
|
|
271
|
-
// plugin disable <plugin-key-or-id>
|
|
272
|
-
// -------------------------------------------------------------------------
|
|
273
|
-
addCommonClientOptions(
|
|
274
|
-
plugin
|
|
275
|
-
.command("disable <pluginKey>")
|
|
276
|
-
.description("Disable a running plugin without uninstalling it")
|
|
277
|
-
.action(async (pluginKey: string, opts: BaseClientOptions) => {
|
|
278
|
-
try {
|
|
279
|
-
const ctx = resolveCommandContext(opts);
|
|
280
|
-
const result = await ctx.api.post<PluginRecord>(
|
|
281
|
-
`/api/plugins/${encodeURIComponent(pluginKey)}/disable`,
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
if (ctx.json) {
|
|
285
|
-
printOutput(result, { json: true });
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
console.log(pc.dim(`Disabled ${pc.bold(pluginKey)} — status: ${result?.status ?? "unknown"}`));
|
|
290
|
-
} catch (err) {
|
|
291
|
-
handleCommandError(err);
|
|
292
|
-
}
|
|
293
|
-
}),
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
// -------------------------------------------------------------------------
|
|
297
|
-
// plugin inspect <plugin-key-or-id>
|
|
298
|
-
// -------------------------------------------------------------------------
|
|
299
|
-
addCommonClientOptions(
|
|
300
|
-
plugin
|
|
301
|
-
.command("inspect <pluginKey>")
|
|
302
|
-
.description("Show full details for an installed plugin")
|
|
303
|
-
.action(async (pluginKey: string, opts: BaseClientOptions) => {
|
|
304
|
-
try {
|
|
305
|
-
const ctx = resolveCommandContext(opts);
|
|
306
|
-
const result = await ctx.api.get<PluginRecord>(
|
|
307
|
-
`/api/plugins/${encodeURIComponent(pluginKey)}`,
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
if (ctx.json) {
|
|
311
|
-
printOutput(result, { json: true });
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (!result) {
|
|
316
|
-
console.log(pc.red(`Plugin not found: ${pluginKey}`));
|
|
317
|
-
process.exit(1);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
console.log(formatPlugin(result));
|
|
321
|
-
if (result.lastError) {
|
|
322
|
-
console.log(`\n${pc.red("Last error:")}\n${result.lastError}`);
|
|
323
|
-
}
|
|
324
|
-
} catch (err) {
|
|
325
|
-
handleCommandError(err);
|
|
326
|
-
}
|
|
327
|
-
}),
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
// -------------------------------------------------------------------------
|
|
331
|
-
// plugin examples
|
|
332
|
-
// -------------------------------------------------------------------------
|
|
333
|
-
addCommonClientOptions(
|
|
334
|
-
plugin
|
|
335
|
-
.command("examples")
|
|
336
|
-
.description("List bundled example plugins available for local install")
|
|
337
|
-
.action(async (opts: BaseClientOptions) => {
|
|
338
|
-
try {
|
|
339
|
-
const ctx = resolveCommandContext(opts);
|
|
340
|
-
const examples = await ctx.api.get<
|
|
341
|
-
Array<{
|
|
342
|
-
packageName: string;
|
|
343
|
-
pluginKey: string;
|
|
344
|
-
displayName: string;
|
|
345
|
-
description: string;
|
|
346
|
-
localPath: string;
|
|
347
|
-
tag: string;
|
|
348
|
-
}>
|
|
349
|
-
>("/api/plugins/examples");
|
|
350
|
-
|
|
351
|
-
if (ctx.json) {
|
|
352
|
-
printOutput(examples, { json: true });
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const rows = examples ?? [];
|
|
357
|
-
if (rows.length === 0) {
|
|
358
|
-
console.log(pc.dim("No bundled examples available."));
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
for (const ex of rows) {
|
|
363
|
-
console.log(
|
|
364
|
-
`${pc.bold(ex.displayName)} ${pc.dim(ex.pluginKey)}\n` +
|
|
365
|
-
` ${ex.description}\n` +
|
|
366
|
-
` ${pc.cyan(`paperclipai plugin install ${ex.localPath}`)}`,
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
} catch (err) {
|
|
370
|
-
handleCommandError(err);
|
|
371
|
-
}
|
|
372
|
-
}),
|
|
373
|
-
);
|
|
374
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { inflateRawSync } from "node:zlib";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { CompanyPortabilityFileEntry } from "@paperclipai/shared";
|
|
4
|
-
|
|
5
|
-
const textDecoder = new TextDecoder();
|
|
6
|
-
|
|
7
|
-
export const binaryContentTypeByExtension: Record<string, string> = {
|
|
8
|
-
".gif": "image/gif",
|
|
9
|
-
".jpeg": "image/jpeg",
|
|
10
|
-
".jpg": "image/jpeg",
|
|
11
|
-
".png": "image/png",
|
|
12
|
-
".svg": "image/svg+xml",
|
|
13
|
-
".webp": "image/webp",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
function normalizeArchivePath(pathValue: string) {
|
|
17
|
-
return pathValue
|
|
18
|
-
.replace(/\\/g, "/")
|
|
19
|
-
.split("/")
|
|
20
|
-
.filter(Boolean)
|
|
21
|
-
.join("/");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function readUint16(source: Uint8Array, offset: number) {
|
|
25
|
-
return source[offset]! | (source[offset + 1]! << 8);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function readUint32(source: Uint8Array, offset: number) {
|
|
29
|
-
return (
|
|
30
|
-
source[offset]! |
|
|
31
|
-
(source[offset + 1]! << 8) |
|
|
32
|
-
(source[offset + 2]! << 16) |
|
|
33
|
-
(source[offset + 3]! << 24)
|
|
34
|
-
) >>> 0;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function sharedArchiveRoot(paths: string[]) {
|
|
38
|
-
if (paths.length === 0) return null;
|
|
39
|
-
const firstSegments = paths
|
|
40
|
-
.map((entry) => normalizeArchivePath(entry).split("/").filter(Boolean))
|
|
41
|
-
.filter((parts) => parts.length > 0);
|
|
42
|
-
if (firstSegments.length === 0) return null;
|
|
43
|
-
const candidate = firstSegments[0]![0]!;
|
|
44
|
-
return firstSegments.every((parts) => parts.length > 1 && parts[0] === candidate)
|
|
45
|
-
? candidate
|
|
46
|
-
: null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function bytesToPortableFileEntry(pathValue: string, bytes: Uint8Array): CompanyPortabilityFileEntry {
|
|
50
|
-
const contentType = binaryContentTypeByExtension[path.extname(pathValue).toLowerCase()];
|
|
51
|
-
if (!contentType) return textDecoder.decode(bytes);
|
|
52
|
-
return {
|
|
53
|
-
encoding: "base64",
|
|
54
|
-
data: Buffer.from(bytes).toString("base64"),
|
|
55
|
-
contentType,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function inflateZipEntry(compressionMethod: number, bytes: Uint8Array) {
|
|
60
|
-
if (compressionMethod === 0) return bytes;
|
|
61
|
-
if (compressionMethod !== 8) {
|
|
62
|
-
throw new Error("Unsupported zip archive: only STORE and DEFLATE entries are supported.");
|
|
63
|
-
}
|
|
64
|
-
return new Uint8Array(inflateRawSync(bytes));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function readZipArchive(source: ArrayBuffer | Uint8Array): Promise<{
|
|
68
|
-
rootPath: string | null;
|
|
69
|
-
files: Record<string, CompanyPortabilityFileEntry>;
|
|
70
|
-
}> {
|
|
71
|
-
const bytes = source instanceof Uint8Array ? source : new Uint8Array(source);
|
|
72
|
-
const entries: Array<{ path: string; body: CompanyPortabilityFileEntry }> = [];
|
|
73
|
-
let offset = 0;
|
|
74
|
-
|
|
75
|
-
while (offset + 4 <= bytes.length) {
|
|
76
|
-
const signature = readUint32(bytes, offset);
|
|
77
|
-
if (signature === 0x02014b50 || signature === 0x06054b50) break;
|
|
78
|
-
if (signature !== 0x04034b50) {
|
|
79
|
-
throw new Error("Invalid zip archive: unsupported local file header.");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (offset + 30 > bytes.length) {
|
|
83
|
-
throw new Error("Invalid zip archive: truncated local file header.");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const generalPurposeFlag = readUint16(bytes, offset + 6);
|
|
87
|
-
const compressionMethod = readUint16(bytes, offset + 8);
|
|
88
|
-
const compressedSize = readUint32(bytes, offset + 18);
|
|
89
|
-
const fileNameLength = readUint16(bytes, offset + 26);
|
|
90
|
-
const extraFieldLength = readUint16(bytes, offset + 28);
|
|
91
|
-
|
|
92
|
-
if ((generalPurposeFlag & 0x0008) !== 0) {
|
|
93
|
-
throw new Error("Unsupported zip archive: data descriptors are not supported.");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const nameOffset = offset + 30;
|
|
97
|
-
const bodyOffset = nameOffset + fileNameLength + extraFieldLength;
|
|
98
|
-
const bodyEnd = bodyOffset + compressedSize;
|
|
99
|
-
if (bodyEnd > bytes.length) {
|
|
100
|
-
throw new Error("Invalid zip archive: truncated file contents.");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const rawArchivePath = textDecoder.decode(bytes.slice(nameOffset, nameOffset + fileNameLength));
|
|
104
|
-
const archivePath = normalizeArchivePath(rawArchivePath);
|
|
105
|
-
const isDirectoryEntry = /\/$/.test(rawArchivePath.replace(/\\/g, "/"));
|
|
106
|
-
if (archivePath && !isDirectoryEntry) {
|
|
107
|
-
const entryBytes = await inflateZipEntry(compressionMethod, bytes.slice(bodyOffset, bodyEnd));
|
|
108
|
-
entries.push({
|
|
109
|
-
path: archivePath,
|
|
110
|
-
body: bytesToPortableFileEntry(archivePath, entryBytes),
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
offset = bodyEnd;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const rootPath = sharedArchiveRoot(entries.map((entry) => entry.path));
|
|
118
|
-
const files: Record<string, CompanyPortabilityFileEntry> = {};
|
|
119
|
-
for (const entry of entries) {
|
|
120
|
-
const normalizedPath =
|
|
121
|
-
rootPath && entry.path.startsWith(`${rootPath}/`)
|
|
122
|
-
? entry.path.slice(rootPath.length + 1)
|
|
123
|
-
: entry.path;
|
|
124
|
-
if (!normalizedPath) continue;
|
|
125
|
-
files[normalizedPath] = entry.body;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return { rootPath, files };
|
|
129
|
-
}
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import * as p from "@clack/prompts";
|
|
2
|
-
import pc from "picocolors";
|
|
3
|
-
import { readConfig, writeConfig, configExists, resolveConfigPath } from "../config/store.js";
|
|
4
|
-
import type { PaperclipConfig } from "../config/schema.js";
|
|
5
|
-
import { ensureLocalSecretsKeyFile } from "../config/secrets-key.js";
|
|
6
|
-
import { promptDatabase } from "../prompts/database.js";
|
|
7
|
-
import { promptLlm } from "../prompts/llm.js";
|
|
8
|
-
import { promptLogging } from "../prompts/logging.js";
|
|
9
|
-
import { defaultSecretsConfig, promptSecrets } from "../prompts/secrets.js";
|
|
10
|
-
import { defaultStorageConfig, promptStorage } from "../prompts/storage.js";
|
|
11
|
-
import { promptServer } from "../prompts/server.js";
|
|
12
|
-
import {
|
|
13
|
-
resolveDefaultBackupDir,
|
|
14
|
-
resolveDefaultEmbeddedPostgresDir,
|
|
15
|
-
resolveDefaultLogsDir,
|
|
16
|
-
resolvePaperclipInstanceId,
|
|
17
|
-
} from "../config/home.js";
|
|
18
|
-
import { printPaperclipCliBanner } from "../utils/banner.js";
|
|
19
|
-
|
|
20
|
-
type Section = "llm" | "database" | "logging" | "server" | "storage" | "secrets";
|
|
21
|
-
|
|
22
|
-
const SECTION_LABELS: Record<Section, string> = {
|
|
23
|
-
llm: "LLM Provider",
|
|
24
|
-
database: "Database",
|
|
25
|
-
logging: "Logging",
|
|
26
|
-
server: "Server",
|
|
27
|
-
storage: "Storage",
|
|
28
|
-
secrets: "Secrets",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
function defaultConfig(): PaperclipConfig {
|
|
32
|
-
const instanceId = resolvePaperclipInstanceId();
|
|
33
|
-
return {
|
|
34
|
-
$meta: {
|
|
35
|
-
version: 1,
|
|
36
|
-
updatedAt: new Date().toISOString(),
|
|
37
|
-
source: "configure",
|
|
38
|
-
},
|
|
39
|
-
database: {
|
|
40
|
-
mode: "embedded-postgres",
|
|
41
|
-
embeddedPostgresDataDir: resolveDefaultEmbeddedPostgresDir(instanceId),
|
|
42
|
-
embeddedPostgresPort: 54329,
|
|
43
|
-
backup: {
|
|
44
|
-
enabled: true,
|
|
45
|
-
intervalMinutes: 60,
|
|
46
|
-
retentionDays: 30,
|
|
47
|
-
dir: resolveDefaultBackupDir(instanceId),
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
logging: {
|
|
51
|
-
mode: "file",
|
|
52
|
-
logDir: resolveDefaultLogsDir(instanceId),
|
|
53
|
-
},
|
|
54
|
-
server: {
|
|
55
|
-
deploymentMode: "local_trusted",
|
|
56
|
-
exposure: "private",
|
|
57
|
-
bind: "loopback",
|
|
58
|
-
host: "127.0.0.1",
|
|
59
|
-
port: 3100,
|
|
60
|
-
allowedHostnames: [],
|
|
61
|
-
serveUi: true,
|
|
62
|
-
},
|
|
63
|
-
auth: {
|
|
64
|
-
baseUrlMode: "auto",
|
|
65
|
-
disableSignUp: false,
|
|
66
|
-
},
|
|
67
|
-
telemetry: {
|
|
68
|
-
enabled: true,
|
|
69
|
-
},
|
|
70
|
-
storage: defaultStorageConfig(),
|
|
71
|
-
secrets: defaultSecretsConfig(),
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export async function configure(opts: {
|
|
76
|
-
config?: string;
|
|
77
|
-
section?: string;
|
|
78
|
-
}): Promise<void> {
|
|
79
|
-
printPaperclipCliBanner();
|
|
80
|
-
p.intro(pc.bgCyan(pc.black(" paperclip configure ")));
|
|
81
|
-
const configPath = resolveConfigPath(opts.config);
|
|
82
|
-
|
|
83
|
-
if (!configExists(opts.config)) {
|
|
84
|
-
p.log.error("No config file found. Run `paperclipai onboard` first.");
|
|
85
|
-
p.outro("");
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
let config: PaperclipConfig;
|
|
90
|
-
try {
|
|
91
|
-
config = readConfig(opts.config) ?? defaultConfig();
|
|
92
|
-
} catch (err) {
|
|
93
|
-
p.log.message(
|
|
94
|
-
pc.yellow(
|
|
95
|
-
`Existing config is invalid. Loading defaults so you can repair it now.\n${err instanceof Error ? err.message : String(err)}`,
|
|
96
|
-
),
|
|
97
|
-
);
|
|
98
|
-
config = defaultConfig();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let section: Section | undefined = opts.section as Section | undefined;
|
|
102
|
-
|
|
103
|
-
if (section && !SECTION_LABELS[section]) {
|
|
104
|
-
p.log.error(`Unknown section: ${section}. Choose from: ${Object.keys(SECTION_LABELS).join(", ")}`);
|
|
105
|
-
p.outro("");
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Section selection loop
|
|
110
|
-
let continueLoop = true;
|
|
111
|
-
while (continueLoop) {
|
|
112
|
-
if (!section) {
|
|
113
|
-
const choice = await p.select({
|
|
114
|
-
message: "Which section do you want to configure?",
|
|
115
|
-
options: Object.entries(SECTION_LABELS).map(([value, label]) => ({
|
|
116
|
-
value: value as Section,
|
|
117
|
-
label,
|
|
118
|
-
})),
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
if (p.isCancel(choice)) {
|
|
122
|
-
p.cancel("Configuration cancelled.");
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
section = choice;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
p.log.step(pc.bold(SECTION_LABELS[section]));
|
|
130
|
-
|
|
131
|
-
switch (section) {
|
|
132
|
-
case "database":
|
|
133
|
-
config.database = await promptDatabase(config.database);
|
|
134
|
-
break;
|
|
135
|
-
case "llm": {
|
|
136
|
-
const llm = await promptLlm();
|
|
137
|
-
if (llm) {
|
|
138
|
-
config.llm = llm;
|
|
139
|
-
} else {
|
|
140
|
-
delete config.llm;
|
|
141
|
-
}
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
case "logging":
|
|
145
|
-
config.logging = await promptLogging();
|
|
146
|
-
break;
|
|
147
|
-
case "server":
|
|
148
|
-
{
|
|
149
|
-
const { server, auth } = await promptServer({
|
|
150
|
-
currentServer: config.server,
|
|
151
|
-
currentAuth: config.auth,
|
|
152
|
-
});
|
|
153
|
-
config.server = server;
|
|
154
|
-
config.auth = auth;
|
|
155
|
-
}
|
|
156
|
-
break;
|
|
157
|
-
case "storage":
|
|
158
|
-
config.storage = await promptStorage(config.storage);
|
|
159
|
-
break;
|
|
160
|
-
case "secrets":
|
|
161
|
-
config.secrets = await promptSecrets(config.secrets);
|
|
162
|
-
{
|
|
163
|
-
const keyResult = ensureLocalSecretsKeyFile(config, configPath);
|
|
164
|
-
if (keyResult.status === "created") {
|
|
165
|
-
p.log.success(`Created local secrets key file at ${pc.dim(keyResult.path)}`);
|
|
166
|
-
} else if (keyResult.status === "existing") {
|
|
167
|
-
p.log.message(pc.dim(`Using existing local secrets key file at ${keyResult.path}`));
|
|
168
|
-
} else if (keyResult.status === "skipped_provider") {
|
|
169
|
-
p.log.message(pc.dim("Skipping local key file management for non-local provider"));
|
|
170
|
-
} else {
|
|
171
|
-
p.log.message(pc.dim("Skipping local key file management because PAPERCLIP_SECRETS_MASTER_KEY is set"));
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
config.$meta.updatedAt = new Date().toISOString();
|
|
178
|
-
config.$meta.source = "configure";
|
|
179
|
-
|
|
180
|
-
writeConfig(config, opts.config);
|
|
181
|
-
p.log.success(`${SECTION_LABELS[section]} configuration updated.`);
|
|
182
|
-
|
|
183
|
-
// If section was provided via CLI flag, don't loop
|
|
184
|
-
if (opts.section) {
|
|
185
|
-
continueLoop = false;
|
|
186
|
-
} else {
|
|
187
|
-
const another = await p.confirm({
|
|
188
|
-
message: "Configure another section?",
|
|
189
|
-
initialValue: false,
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
if (p.isCancel(another) || !another) {
|
|
193
|
-
continueLoop = false;
|
|
194
|
-
} else {
|
|
195
|
-
section = undefined; // Reset to show picker again
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
p.outro("Configuration saved.");
|
|
201
|
-
}
|