@irogane/kaji 0.2.0-beta.11
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/LICENSE +105 -0
- package/README.md +598 -0
- package/contracts/README.md +14 -0
- package/contracts/beta-core-v1.json +52 -0
- package/contracts/cli/init-cases-v1.json +27 -0
- package/contracts/errors/error-codes.json +48 -0
- package/contracts/errors/integration-recovery-v1.json +127 -0
- package/contracts/errors/provider-normalization.json +111 -0
- package/contracts/events/conformance-invalid.json +20 -0
- package/contracts/events/conformance.json +511 -0
- package/contracts/events/new-kaji-event-v1.schema.json +1021 -0
- package/contracts/events/stored-kaji-event-v1.schema.json +1025 -0
- package/contracts/feature-tiers-v1.json +488 -0
- package/contracts/integrations/abi-index-v1.json +8 -0
- package/contracts/integrations/conformance-invalid.json +443 -0
- package/contracts/integrations/conformance-valid.json +121 -0
- package/contracts/integrations/copy-provenance-v1.schema.json +61 -0
- package/contracts/integrations/echo-tool-abi-v1.json +37 -0
- package/contracts/integrations/github-api-conformance-v1.json +644 -0
- package/contracts/integrations/github-tool-abi-typescript-v1.json +369 -0
- package/contracts/integrations/github-tool-abi-v1.json +146 -0
- package/contracts/integrations/gmail-api-conformance-v1.json +750 -0
- package/contracts/integrations/gmail-tool-abi-v1.json +62 -0
- package/contracts/integrations/index.schema.json +37 -0
- package/contracts/integrations/manifest.schema.json +119 -0
- package/contracts/parity/expected-normalized.json +4906 -0
- package/contracts/parity/scenarios.json +100 -0
- package/contracts/parity/scenarios.schema.json +214 -0
- package/contracts/providers/cost-conformance.json +112 -0
- package/contracts/release/github-proof-v1.schema.json +138 -0
- package/contracts/release/gmail-proof-v1.schema.json +138 -0
- package/contracts/release/kaji-ts-consumer-handoff-v1.schema.json +1289 -0
- package/contracts/release/publisher-identity-receipt-v1.schema.json +319 -0
- package/contracts/release/typescript-onboarding-evidence-v1.schema.json +740 -0
- package/contracts/tools/conformance-invalid.json +271 -0
- package/contracts/tools/conformance-valid.json +78 -0
- package/contracts/tools/tool-schema-v1.schema.json +18 -0
- package/dist/anthropic.cjs +1231 -0
- package/dist/anthropic.cjs.map +1 -0
- package/dist/anthropic.d.cts +26 -0
- package/dist/anthropic.d.ts +26 -0
- package/dist/anthropic.js +270 -0
- package/dist/anthropic.js.map +1 -0
- package/dist/auth.cjs +1507 -0
- package/dist/auth.cjs.map +1 -0
- package/dist/auth.d.cts +129 -0
- package/dist/auth.d.ts +129 -0
- package/dist/auth.js +1039 -0
- package/dist/auth.js.map +1 -0
- package/dist/base-B9FRMcP8.d.cts +140 -0
- package/dist/base-nHQd1VtS.d.ts +140 -0
- package/dist/chunk-AAM33KAO.js +4367 -0
- package/dist/chunk-AAM33KAO.js.map +1 -0
- package/dist/chunk-KAJ6BM64.js +153 -0
- package/dist/chunk-KAJ6BM64.js.map +1 -0
- package/dist/chunk-KCAXIOZS.js +308 -0
- package/dist/chunk-KCAXIOZS.js.map +1 -0
- package/dist/chunk-LSJ4AVO2.js +243 -0
- package/dist/chunk-LSJ4AVO2.js.map +1 -0
- package/dist/chunk-TM7ZGOJX.js +716 -0
- package/dist/chunk-TM7ZGOJX.js.map +1 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +13 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/chunk-2RCWPRVY.js +6277 -0
- package/dist/cli/chunk-2RCWPRVY.js.map +1 -0
- package/dist/cli/chunk-SEBX54TR.js +681 -0
- package/dist/cli/chunk-SEBX54TR.js.map +1 -0
- package/dist/cli/index.d.ts +232 -0
- package/dist/cli/index.js +11 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-worker.d.ts +2 -0
- package/dist/cli/init-worker.js +18 -0
- package/dist/cli/init-worker.js.map +1 -0
- package/dist/cli/integration-copy-worker.js +48 -0
- package/dist/cli/integration-copy-worker.js.map +1 -0
- package/dist/cli/package-entry-cjs.cjs +21 -0
- package/dist/cli/package-entry-cjs.cjs.map +1 -0
- package/dist/cli/package-entry-cjs.d.cts +2 -0
- package/dist/cli/package-entry.d.ts +2 -0
- package/dist/cli/package-entry.js +13 -0
- package/dist/cli/package-entry.js.map +1 -0
- package/dist/context-BaFHrQHv.d.cts +21 -0
- package/dist/context-BaFHrQHv.d.ts +21 -0
- package/dist/context-C-YPY-GS.d.cts +1538 -0
- package/dist/context-C-YPY-GS.d.ts +1538 -0
- package/dist/index.cjs +12346 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1560 -0
- package/dist/index.d.ts +1560 -0
- package/dist/index.js +7852 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/github.cjs +2092 -0
- package/dist/integrations/github.cjs.map +1 -0
- package/dist/integrations/github.d.cts +21 -0
- package/dist/integrations/github.d.ts +21 -0
- package/dist/integrations/github.js +2088 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations.cjs +3370 -0
- package/dist/integrations.cjs.map +1 -0
- package/dist/integrations.d.cts +202 -0
- package/dist/integrations.d.ts +202 -0
- package/dist/integrations.js +2650 -0
- package/dist/integrations.js.map +1 -0
- package/dist/observability-Cj--OkME.d.cts +96 -0
- package/dist/observability-Cj--OkME.d.ts +96 -0
- package/dist/openai.cjs +1235 -0
- package/dist/openai.cjs.map +1 -0
- package/dist/openai.d.cts +32 -0
- package/dist/openai.d.ts +32 -0
- package/dist/openai.js +272 -0
- package/dist/openai.js.map +1 -0
- package/dist/testing.cjs +554 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +43 -0
- package/dist/testing.d.ts +43 -0
- package/dist/testing.js +118 -0
- package/dist/testing.js.map +1 -0
- package/package.json +188 -0
- package/registry/echo/index.ts +53 -0
- package/registry/echo/manifest.json +53 -0
- package/registry/github/LICENSE +105 -0
- package/registry/github/client.ts +1727 -0
- package/registry/github/index.ts +263 -0
- package/registry/github/manifest.json +227 -0
- package/registry/github/owner-fixtures.json +10 -0
- package/registry/github/tests/github.test.ts +32 -0
- package/registry/gmail/LICENSE +105 -0
- package/registry/gmail/client.ts +548 -0
- package/registry/gmail/index.ts +165 -0
- package/registry/gmail/manifest.json +102 -0
- package/registry/gmail/owner-fixtures.json +10 -0
- package/registry/gmail/tests/gmail.test.ts +32 -0
- package/registry/index.json +21 -0
- package/registry/index.schema.json +37 -0
- package/registry/schema.json +119 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/init.ts
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { once } from "events";
|
|
7
|
+
import { constants, existsSync, lstatSync, mkdirSync, readFileSync } from "fs";
|
|
8
|
+
import { link, lstat as lstatAsync, mkdtemp, open, rename, rm, writeFile } from "fs/promises";
|
|
9
|
+
import { basename, isAbsolute, join, resolve } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
var PROVIDERS = ["mock", "openai", "anthropic"];
|
|
12
|
+
var CliArgError = class extends Error {
|
|
13
|
+
};
|
|
14
|
+
var WORKER_INPUT_LIMIT_BYTES = 256 * 1024;
|
|
15
|
+
var WORKER_OUTPUT_LIMIT_BYTES = 8 * 1024;
|
|
16
|
+
var WORKER_TIMEOUT_MS = 15e3;
|
|
17
|
+
var WORKER_COMMIT = "commit";
|
|
18
|
+
var WORKER_CANCEL = "cancel";
|
|
19
|
+
var WORKER_PREPARED = '{"prepared":true}';
|
|
20
|
+
var WORKER_SUCCESS = '{"ok":true}';
|
|
21
|
+
var RECOVERY_DIRECTORY = /^\.kaji-scaffold-backup-[A-Za-z0-9]{6}$/;
|
|
22
|
+
function safeRecoveryDirectory(value) {
|
|
23
|
+
return typeof value === "string" && RECOVERY_DIRECTORY.test(value) ? value : void 0;
|
|
24
|
+
}
|
|
25
|
+
var ScaffoldRollbackError = class extends Error {
|
|
26
|
+
recoveryDirectory;
|
|
27
|
+
constructor(recoveryDirectory, cause) {
|
|
28
|
+
const safeDirectory = safeRecoveryDirectory(recoveryDirectory);
|
|
29
|
+
if (safeDirectory === void 0) throw new Error("invalid scaffold recovery directory");
|
|
30
|
+
super("scaffold publication rollback failed", { cause });
|
|
31
|
+
this.name = "ScaffoldRollbackError";
|
|
32
|
+
this.recoveryDirectory = safeDirectory;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function scaffoldRecoveryDirectory(error) {
|
|
36
|
+
return error instanceof ScaffoldRollbackError ? error.recoveryDirectory : void 0;
|
|
37
|
+
}
|
|
38
|
+
function throwIfCancelled(signal) {
|
|
39
|
+
if (signal?.aborted) throw new Error("scaffold worker cancelled");
|
|
40
|
+
}
|
|
41
|
+
function parseArgs(rest) {
|
|
42
|
+
let positionalPath;
|
|
43
|
+
let provider = "mock";
|
|
44
|
+
let force = false;
|
|
45
|
+
for (let index = 0; index < rest.length; index++) {
|
|
46
|
+
const arg = rest[index];
|
|
47
|
+
if (arg === "--provider") {
|
|
48
|
+
const next = rest[index + 1];
|
|
49
|
+
if (next === void 0 || next.startsWith("--")) {
|
|
50
|
+
throw new CliArgError("--provider requires a value");
|
|
51
|
+
}
|
|
52
|
+
if (!PROVIDERS.includes(next)) {
|
|
53
|
+
throw new CliArgError("--provider must be mock, openai, or anthropic");
|
|
54
|
+
}
|
|
55
|
+
provider = next;
|
|
56
|
+
index++;
|
|
57
|
+
} else if (arg === "--force") {
|
|
58
|
+
force = true;
|
|
59
|
+
} else if (arg === "--yes") {
|
|
60
|
+
} else if (arg.startsWith("--")) {
|
|
61
|
+
throw new CliArgError(`unknown argument: ${arg}`);
|
|
62
|
+
} else if (positionalPath === void 0) {
|
|
63
|
+
positionalPath = arg;
|
|
64
|
+
} else {
|
|
65
|
+
throw new CliArgError(`unexpected path argument: ${arg}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
out: resolve(positionalPath ?? "."),
|
|
70
|
+
provider,
|
|
71
|
+
force
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function installedMetadata() {
|
|
75
|
+
const path = new URL("../../package.json", import.meta.url);
|
|
76
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
77
|
+
if (typeof value.version !== "string" || value.version.length === 0 || value.peerDependencies === void 0 || value.devDependencies === void 0) {
|
|
78
|
+
throw new Error("installed kaji package metadata is incomplete");
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function agentSource(provider) {
|
|
83
|
+
const providerImports = {
|
|
84
|
+
mock: 'import { MockProvider } from "@irogane/kaji/testing";',
|
|
85
|
+
openai: 'import { OpenAIProvider } from "@irogane/kaji/openai";',
|
|
86
|
+
anthropic: 'import { AnthropicProvider } from "@irogane/kaji/anthropic";'
|
|
87
|
+
};
|
|
88
|
+
const providerSetup = {
|
|
89
|
+
mock: "const provider = new MockProvider();",
|
|
90
|
+
openai: `const environment = (globalThis as unknown as { process: { env: Record<string, string | undefined> } }).process.env;
|
|
91
|
+
const apiKey = environment.OPENAI_API_KEY;
|
|
92
|
+
if (!apiKey) throw new Error("OPENAI_API_KEY is required for the openai scaffold");
|
|
93
|
+
const provider = new OpenAIProvider({ apiKey });`,
|
|
94
|
+
anthropic: `const environment = (globalThis as unknown as { process: { env: Record<string, string | undefined> } }).process.env;
|
|
95
|
+
const apiKey = environment.ANTHROPIC_API_KEY;
|
|
96
|
+
if (!apiKey) throw new Error("ANTHROPIC_API_KEY is required for the anthropic scaffold");
|
|
97
|
+
const provider = new AnthropicProvider({ apiKey });`
|
|
98
|
+
};
|
|
99
|
+
return `import { AgentBuilder } from "@irogane/kaji";
|
|
100
|
+
${providerImports[provider]}
|
|
101
|
+
|
|
102
|
+
${providerSetup[provider]}
|
|
103
|
+
const runtime = new AgentBuilder().provider(provider).build();
|
|
104
|
+
const result = await runtime.turn("Say hello.");
|
|
105
|
+
const finalSequence = Math.max(...result.events.map((event) => event.sequence));
|
|
106
|
+
console.log(\`text=\${result.text}\`);
|
|
107
|
+
console.log(\`turn_id=\${result.turnId}\`);
|
|
108
|
+
console.log(\`final_sequence=\${finalSequence}\`);
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
function scaffoldFiles(provider) {
|
|
112
|
+
const metadata = installedMetadata();
|
|
113
|
+
const zodRange = metadata.peerDependencies.zod;
|
|
114
|
+
if (zodRange === void 0) throw new Error("installed kaji has no Zod peer range");
|
|
115
|
+
const nodeTypesRange = metadata.devDependencies["@types/node"];
|
|
116
|
+
if (nodeTypesRange === void 0) {
|
|
117
|
+
throw new Error("installed kaji has no supported @types/node range");
|
|
118
|
+
}
|
|
119
|
+
const dotenvxVersion = metadata.devDependencies["@dotenvx/dotenvx"];
|
|
120
|
+
if (dotenvxVersion === void 0) {
|
|
121
|
+
throw new Error("installed kaji has no supported dotenvx version");
|
|
122
|
+
}
|
|
123
|
+
const dependencies = {
|
|
124
|
+
"@irogane/kaji": metadata.version,
|
|
125
|
+
zod: zodRange
|
|
126
|
+
};
|
|
127
|
+
if (provider === "openai") {
|
|
128
|
+
const range = metadata.peerDependencies.openai;
|
|
129
|
+
if (range === void 0) throw new Error("installed kaji has no OpenAI peer range");
|
|
130
|
+
dependencies.openai = range;
|
|
131
|
+
} else if (provider === "anthropic") {
|
|
132
|
+
const range = metadata.peerDependencies["@anthropic-ai/sdk"];
|
|
133
|
+
if (range === void 0) throw new Error("installed kaji has no Anthropic peer range");
|
|
134
|
+
dependencies["@anthropic-ai/sdk"] = range;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
"package.json": JSON.stringify(
|
|
138
|
+
{
|
|
139
|
+
name: "my-kaji-agent",
|
|
140
|
+
version: "0.1.0",
|
|
141
|
+
private: true,
|
|
142
|
+
type: "module",
|
|
143
|
+
scripts: {
|
|
144
|
+
start: "dotenvx run --ignore=MISSING_ENV_FILE -- tsx agent.ts",
|
|
145
|
+
typecheck: "tsc --noEmit"
|
|
146
|
+
},
|
|
147
|
+
dependencies,
|
|
148
|
+
devDependencies: {
|
|
149
|
+
"@dotenvx/dotenvx": dotenvxVersion,
|
|
150
|
+
"@types/node": nodeTypesRange,
|
|
151
|
+
tsx: "^4.0.0",
|
|
152
|
+
typescript57: metadata.devDependencies.typescript57 ?? "npm:typescript@5.7.3",
|
|
153
|
+
typescript: metadata.devDependencies.typescript ?? "^6.0.0"
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
null,
|
|
157
|
+
2
|
|
158
|
+
),
|
|
159
|
+
"tsconfig.json": JSON.stringify(
|
|
160
|
+
{
|
|
161
|
+
compilerOptions: {
|
|
162
|
+
target: "ES2022",
|
|
163
|
+
module: "ESNext",
|
|
164
|
+
moduleResolution: "Bundler",
|
|
165
|
+
strict: true,
|
|
166
|
+
esModuleInterop: true,
|
|
167
|
+
skipLibCheck: false,
|
|
168
|
+
types: ["node"]
|
|
169
|
+
},
|
|
170
|
+
include: ["*.ts"]
|
|
171
|
+
},
|
|
172
|
+
null,
|
|
173
|
+
2
|
|
174
|
+
),
|
|
175
|
+
"agent.ts": agentSource(provider),
|
|
176
|
+
".env.example": provider === "mock" ? "# No provider credentials required.\n" : provider === "openai" ? "OPENAI_API_KEY=\n" : "ANTHROPIC_API_KEY=\n"
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function destinationKind(path) {
|
|
180
|
+
try {
|
|
181
|
+
const stat = lstatSync(path);
|
|
182
|
+
if (stat.isSymbolicLink()) return "symlink";
|
|
183
|
+
return stat.isFile() ? "file" : "other";
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error.code === "ENOENT") return "missing";
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function validateWorkerRequest(value) {
|
|
190
|
+
if (typeof value !== "object" || value === null) {
|
|
191
|
+
throw new Error("invalid scaffold worker request");
|
|
192
|
+
}
|
|
193
|
+
const request = value;
|
|
194
|
+
if (request.version !== 1 || typeof request.expectedDev !== "string" || !/^\d+$/.test(request.expectedDev) || typeof request.expectedIno !== "string" || !/^\d+$/.test(request.expectedIno) || typeof request.targetAbsolute !== "string" || !isAbsolute(request.targetAbsolute) || typeof request.files !== "object" || request.files === null || Array.isArray(request.files) || typeof request.force !== "boolean") {
|
|
195
|
+
throw new Error("invalid scaffold worker request");
|
|
196
|
+
}
|
|
197
|
+
const entries = Object.entries(request.files);
|
|
198
|
+
if (entries.length === 0 || entries.length > 32 || entries.some(
|
|
199
|
+
([name, body]) => name.length === 0 || name === "." || name === ".." || basename(name) !== name || name.includes("\0") || typeof body !== "string"
|
|
200
|
+
)) {
|
|
201
|
+
throw new Error("invalid scaffold worker request");
|
|
202
|
+
}
|
|
203
|
+
return request;
|
|
204
|
+
}
|
|
205
|
+
async function assertTargetIdentity(targetAbsolute, expectedDev, expectedIno) {
|
|
206
|
+
let targetStat;
|
|
207
|
+
try {
|
|
208
|
+
targetStat = await lstatAsync(targetAbsolute, { bigint: true });
|
|
209
|
+
} catch {
|
|
210
|
+
throw new Error("target directory changed during scaffold publication");
|
|
211
|
+
}
|
|
212
|
+
if (!targetStat.isDirectory() || targetStat.isSymbolicLink() || targetStat.dev !== expectedDev || targetStat.ino !== expectedIno) {
|
|
213
|
+
throw new Error("target directory changed during scaffold publication");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function runInitWorkerRequest(requestValue, options = {}) {
|
|
217
|
+
const request = validateWorkerRequest(requestValue);
|
|
218
|
+
const expectedDev = BigInt(request.expectedDev);
|
|
219
|
+
const expectedIno = BigInt(request.expectedIno);
|
|
220
|
+
throwIfCancelled(options.signal);
|
|
221
|
+
const cwdStat = await lstatAsync(".", { bigint: true });
|
|
222
|
+
throwIfCancelled(options.signal);
|
|
223
|
+
if (!cwdStat.isDirectory() || cwdStat.dev !== expectedDev || cwdStat.ino !== expectedIno) {
|
|
224
|
+
throw new Error("pinned scaffold directory identity mismatch");
|
|
225
|
+
}
|
|
226
|
+
await options.afterHandshake?.();
|
|
227
|
+
throwIfCancelled(options.signal);
|
|
228
|
+
await writeScaffoldFiles(".", request.files, request.force, {
|
|
229
|
+
beforePublish: options.beforePublish,
|
|
230
|
+
publish: options.publish,
|
|
231
|
+
signal: options.signal,
|
|
232
|
+
verifyTarget: () => assertTargetIdentity(request.targetAbsolute, expectedDev, expectedIno)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
var WorkerInput = class {
|
|
236
|
+
buffer = Buffer.alloc(0);
|
|
237
|
+
bytes = 0;
|
|
238
|
+
async readLine(signal) {
|
|
239
|
+
for (; ; ) {
|
|
240
|
+
const newline = this.buffer.indexOf(10);
|
|
241
|
+
if (newline >= 0) {
|
|
242
|
+
const line = this.buffer.subarray(0, newline);
|
|
243
|
+
this.buffer = this.buffer.subarray(newline + 1);
|
|
244
|
+
return line.at(-1) === 13 ? line.subarray(0, line.byteLength - 1).toString("utf8") : line.toString("utf8");
|
|
245
|
+
}
|
|
246
|
+
const value = process.stdin.read();
|
|
247
|
+
if (value !== null) {
|
|
248
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
249
|
+
this.bytes += chunk.byteLength;
|
|
250
|
+
if (this.bytes > WORKER_INPUT_LIMIT_BYTES) {
|
|
251
|
+
throw new Error("scaffold worker input exceeded limit");
|
|
252
|
+
}
|
|
253
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (process.stdin.readableEnded) {
|
|
257
|
+
if (this.buffer.byteLength === 0) throw new Error("scaffold worker input ended early");
|
|
258
|
+
const line = this.buffer.toString("utf8");
|
|
259
|
+
this.buffer = Buffer.alloc(0);
|
|
260
|
+
return line;
|
|
261
|
+
}
|
|
262
|
+
await once(process.stdin, "readable", { signal });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
async function runInitWorkerMain() {
|
|
267
|
+
const controller = new AbortController();
|
|
268
|
+
const cancel = () => controller.abort();
|
|
269
|
+
let timeout = setTimeout(cancel, WORKER_TIMEOUT_MS);
|
|
270
|
+
process.once("SIGTERM", cancel);
|
|
271
|
+
const input = new WorkerInput();
|
|
272
|
+
try {
|
|
273
|
+
const request = JSON.parse(await input.readLine(controller.signal));
|
|
274
|
+
await runInitWorkerRequest(request, {
|
|
275
|
+
signal: controller.signal,
|
|
276
|
+
beforePublish: async () => {
|
|
277
|
+
throwIfCancelled(controller.signal);
|
|
278
|
+
clearTimeout(timeout);
|
|
279
|
+
timeout = setTimeout(cancel, WORKER_TIMEOUT_MS);
|
|
280
|
+
process.stdout.write(`${WORKER_PREPARED}
|
|
281
|
+
`);
|
|
282
|
+
const command = await input.readLine(controller.signal);
|
|
283
|
+
if (command !== WORKER_COMMIT) throw new Error("scaffold worker cancelled");
|
|
284
|
+
throwIfCancelled(controller.signal);
|
|
285
|
+
clearTimeout(timeout);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
} finally {
|
|
289
|
+
clearTimeout(timeout);
|
|
290
|
+
process.removeListener("SIGTERM", cancel);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function workerRecoveryLine(line) {
|
|
294
|
+
try {
|
|
295
|
+
const value = JSON.parse(line);
|
|
296
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
297
|
+
const record = value;
|
|
298
|
+
if (record.ok !== false || Object.keys(record).sort().join(",") !== "ok,recovery") {
|
|
299
|
+
return void 0;
|
|
300
|
+
}
|
|
301
|
+
return safeRecoveryDirectory(record.recovery);
|
|
302
|
+
} catch {
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function safeTimeout(value) {
|
|
307
|
+
return value !== void 0 && Number.isSafeInteger(value) && value > 0 ? value : WORKER_TIMEOUT_MS;
|
|
308
|
+
}
|
|
309
|
+
async function invokeInitWorkerProcess(request, options = {}) {
|
|
310
|
+
const payload = JSON.stringify(request);
|
|
311
|
+
if (Buffer.byteLength(`${payload}
|
|
312
|
+
${WORKER_COMMIT}
|
|
313
|
+
`) > WORKER_INPUT_LIMIT_BYTES) {
|
|
314
|
+
throw new Error("pinned scaffold worker failed");
|
|
315
|
+
}
|
|
316
|
+
const workerPath = fileURLToPath(new URL("./init-worker.js", import.meta.url));
|
|
317
|
+
const workerExecutable = typeof process.versions.bun === "string" ? process.env.NODE_BINARY ?? "node" : process.execPath;
|
|
318
|
+
const workerEnvironment = { ...process.env };
|
|
319
|
+
delete workerEnvironment.NODE_OPTIONS;
|
|
320
|
+
delete workerEnvironment.NODE_PATH;
|
|
321
|
+
const command = options.command ?? [workerExecutable, workerPath];
|
|
322
|
+
const startupTimeoutMs = safeTimeout(options.startupTimeoutMs);
|
|
323
|
+
const transactionTimeoutMs = safeTimeout(options.transactionTimeoutMs);
|
|
324
|
+
await new Promise((resolveWorker, rejectWorker) => {
|
|
325
|
+
const child = spawn(command[0], command.slice(1), {
|
|
326
|
+
cwd: request.targetAbsolute,
|
|
327
|
+
env: workerEnvironment,
|
|
328
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
329
|
+
windowsHide: true
|
|
330
|
+
});
|
|
331
|
+
let stdoutBuffer = "";
|
|
332
|
+
let stdoutBytes = 0;
|
|
333
|
+
let stderrBytes = 0;
|
|
334
|
+
let failed = false;
|
|
335
|
+
let settled = false;
|
|
336
|
+
let prepared = false;
|
|
337
|
+
let succeeded = false;
|
|
338
|
+
let recoveryDirectory;
|
|
339
|
+
let timer;
|
|
340
|
+
const finish = (error) => {
|
|
341
|
+
if (settled) return;
|
|
342
|
+
settled = true;
|
|
343
|
+
clearTimeout(timer);
|
|
344
|
+
if (error === void 0) resolveWorker();
|
|
345
|
+
else rejectWorker(error);
|
|
346
|
+
};
|
|
347
|
+
const cancelWorker = () => {
|
|
348
|
+
if (settled) return;
|
|
349
|
+
failed = true;
|
|
350
|
+
if (!child.stdin.destroyed && child.stdin.writable) child.stdin.end(`${WORKER_CANCEL}
|
|
351
|
+
`);
|
|
352
|
+
child.kill("SIGTERM");
|
|
353
|
+
};
|
|
354
|
+
const armTimeout = (durationMs) => {
|
|
355
|
+
clearTimeout(timer);
|
|
356
|
+
timer = setTimeout(cancelWorker, durationMs);
|
|
357
|
+
};
|
|
358
|
+
const authorizeCommit = async () => {
|
|
359
|
+
try {
|
|
360
|
+
await options.beforeCommit?.();
|
|
361
|
+
} catch {
|
|
362
|
+
cancelWorker();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (!failed && !settled && !child.stdin.destroyed && child.stdin.writable) {
|
|
366
|
+
clearTimeout(timer);
|
|
367
|
+
child.stdin.end(`${WORKER_COMMIT}
|
|
368
|
+
`);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
const consumeLine = (line) => {
|
|
372
|
+
if (!prepared && line === WORKER_PREPARED) {
|
|
373
|
+
prepared = true;
|
|
374
|
+
armTimeout(transactionTimeoutMs);
|
|
375
|
+
void authorizeCommit();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (prepared && !succeeded && line === WORKER_SUCCESS) {
|
|
379
|
+
succeeded = true;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const recovery = workerRecoveryLine(line);
|
|
383
|
+
if (recovery !== void 0 && recoveryDirectory === void 0) {
|
|
384
|
+
recoveryDirectory = recovery;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
cancelWorker();
|
|
388
|
+
};
|
|
389
|
+
const consume = (chunk, stream) => {
|
|
390
|
+
if (stream === "stdout") {
|
|
391
|
+
stdoutBytes += chunk.byteLength;
|
|
392
|
+
if (stdoutBytes <= WORKER_OUTPUT_LIMIT_BYTES) {
|
|
393
|
+
stdoutBuffer += chunk.toString("utf8");
|
|
394
|
+
for (; ; ) {
|
|
395
|
+
const newline = stdoutBuffer.indexOf("\n");
|
|
396
|
+
if (newline < 0) break;
|
|
397
|
+
const line = stdoutBuffer.slice(0, newline);
|
|
398
|
+
stdoutBuffer = stdoutBuffer.slice(newline + 1);
|
|
399
|
+
consumeLine(line);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} else {
|
|
403
|
+
stderrBytes += chunk.byteLength;
|
|
404
|
+
}
|
|
405
|
+
if (stdoutBytes > WORKER_OUTPUT_LIMIT_BYTES || stderrBytes > WORKER_OUTPUT_LIMIT_BYTES) {
|
|
406
|
+
cancelWorker();
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
armTimeout(startupTimeoutMs);
|
|
410
|
+
child.stdout.on("data", (chunk) => consume(chunk, "stdout"));
|
|
411
|
+
child.stderr.on("data", (chunk) => consume(chunk, "stderr"));
|
|
412
|
+
child.stdin.on("error", cancelWorker);
|
|
413
|
+
child.on("error", () => finish(new Error("pinned scaffold worker failed")));
|
|
414
|
+
child.on("close", (code) => {
|
|
415
|
+
if (recoveryDirectory !== void 0) {
|
|
416
|
+
finish(new ScaffoldRollbackError(recoveryDirectory));
|
|
417
|
+
} else if (!failed && code === 0 && prepared && succeeded && stdoutBuffer.length === 0) {
|
|
418
|
+
finish();
|
|
419
|
+
} else {
|
|
420
|
+
finish(new Error("pinned scaffold worker failed"));
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
child.stdin.write(`${payload}
|
|
424
|
+
`);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
async function runPinnedInitWorker(out, files, force, options = {}) {
|
|
428
|
+
const targetAbsolute = resolve(out);
|
|
429
|
+
let directory;
|
|
430
|
+
try {
|
|
431
|
+
directory = await open(
|
|
432
|
+
targetAbsolute,
|
|
433
|
+
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW
|
|
434
|
+
);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (["ELOOP", "ENOTDIR"].includes(error.code ?? "")) {
|
|
437
|
+
throw new Error("unsafe scaffold destination: symbolic links are not allowed");
|
|
438
|
+
}
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const targetStat = await directory.stat({ bigint: true });
|
|
443
|
+
if (!targetStat.isDirectory()) {
|
|
444
|
+
throw new Error("unsafe scaffold destination: target is not a directory");
|
|
445
|
+
}
|
|
446
|
+
const request = {
|
|
447
|
+
version: 1,
|
|
448
|
+
expectedDev: targetStat.dev.toString(),
|
|
449
|
+
expectedIno: targetStat.ino.toString(),
|
|
450
|
+
targetAbsolute,
|
|
451
|
+
files,
|
|
452
|
+
force
|
|
453
|
+
};
|
|
454
|
+
try {
|
|
455
|
+
await (options.invokeWorker ?? invokeInitWorkerProcess)(request);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
if (error instanceof ScaffoldRollbackError) throw error;
|
|
458
|
+
throw new Error("pinned scaffold worker failed");
|
|
459
|
+
}
|
|
460
|
+
} finally {
|
|
461
|
+
await directory.close();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function writeScaffoldFiles(out, files, force, options = {}) {
|
|
465
|
+
const entries = Object.entries(files).map(([name, body]) => ({
|
|
466
|
+
body,
|
|
467
|
+
destination: join(out, name),
|
|
468
|
+
temporary: join(out, `.${name}.kaji-${randomUUID()}.tmp`)
|
|
469
|
+
}));
|
|
470
|
+
const committed = [];
|
|
471
|
+
const backups = [];
|
|
472
|
+
const published = [];
|
|
473
|
+
let backupDirectory;
|
|
474
|
+
try {
|
|
475
|
+
throwIfCancelled(options.signal);
|
|
476
|
+
for (const entry of entries) {
|
|
477
|
+
await writeFile(entry.temporary, entry.body, { encoding: "utf8", flag: "wx" });
|
|
478
|
+
throwIfCancelled(options.signal);
|
|
479
|
+
}
|
|
480
|
+
if (entries.some(({ destination }) => destinationKind(destination) === "symlink")) {
|
|
481
|
+
throw new Error("unsafe scaffold destination: symbolic links are not allowed");
|
|
482
|
+
}
|
|
483
|
+
if (!force && entries.some(({ destination }) => destinationKind(destination) !== "missing")) {
|
|
484
|
+
throw Object.assign(new Error("scaffold destination appeared during write"), {
|
|
485
|
+
code: "EEXIST"
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (force) {
|
|
489
|
+
const kinds = entries.map((entry) => [entry, destinationKind(entry.destination)]);
|
|
490
|
+
if (kinds.some(([, kind]) => kind === "symlink")) {
|
|
491
|
+
throw new Error("unsafe scaffold destination: symbolic links are not allowed");
|
|
492
|
+
}
|
|
493
|
+
if (kinds.some(([, kind]) => kind === "other")) {
|
|
494
|
+
throw new Error("unsafe scaffold destination: generated paths must be regular files");
|
|
495
|
+
}
|
|
496
|
+
backupDirectory = await mkdtemp(join(out, ".kaji-scaffold-backup-"));
|
|
497
|
+
for (const [entry] of kinds) {
|
|
498
|
+
const currentKind = destinationKind(entry.destination);
|
|
499
|
+
if (currentKind === "symlink" || currentKind === "other") {
|
|
500
|
+
throw new Error("unsafe scaffold destination: generated paths must be regular files");
|
|
501
|
+
}
|
|
502
|
+
if (currentKind === "missing") continue;
|
|
503
|
+
const sourceStat = await lstatAsync(entry.destination, { bigint: true });
|
|
504
|
+
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
505
|
+
throw new Error("unsafe scaffold destination: generated paths must be regular files");
|
|
506
|
+
}
|
|
507
|
+
const backup = join(backupDirectory, basename(entry.destination));
|
|
508
|
+
await rename(entry.destination, backup);
|
|
509
|
+
backups.push({ destination: entry.destination, backup });
|
|
510
|
+
throwIfCancelled(options.signal);
|
|
511
|
+
const backupStat = await lstatAsync(backup, { bigint: true });
|
|
512
|
+
throwIfCancelled(options.signal);
|
|
513
|
+
if (!backupStat.isFile() || backupStat.isSymbolicLink() || backupStat.dev !== sourceStat.dev || backupStat.ino !== sourceStat.ino) {
|
|
514
|
+
throw new Error("unsafe scaffold destination: destination changed during backup");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
await options.beforePublish?.();
|
|
518
|
+
throwIfCancelled(options.signal);
|
|
519
|
+
const publish = options.publish ?? rename;
|
|
520
|
+
for (const entry of entries) {
|
|
521
|
+
const temporaryStat = await lstatAsync(entry.temporary, { bigint: true });
|
|
522
|
+
throwIfCancelled(options.signal);
|
|
523
|
+
published.push({
|
|
524
|
+
destination: entry.destination,
|
|
525
|
+
dev: temporaryStat.dev,
|
|
526
|
+
ino: temporaryStat.ino
|
|
527
|
+
});
|
|
528
|
+
await publish(entry.temporary, entry.destination);
|
|
529
|
+
throwIfCancelled(options.signal);
|
|
530
|
+
const destinationStat = await lstatAsync(entry.destination, { bigint: true });
|
|
531
|
+
throwIfCancelled(options.signal);
|
|
532
|
+
if (!destinationStat.isFile() || destinationStat.isSymbolicLink() || destinationStat.dev !== temporaryStat.dev || destinationStat.ino !== temporaryStat.ino) {
|
|
533
|
+
throw new Error("unsafe scaffold destination: destination changed during publication");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
await options.verifyTarget?.();
|
|
537
|
+
throwIfCancelled(options.signal);
|
|
538
|
+
await rm(backupDirectory, { recursive: true, force: true });
|
|
539
|
+
backupDirectory = void 0;
|
|
540
|
+
} else {
|
|
541
|
+
await options.beforePublish?.();
|
|
542
|
+
throwIfCancelled(options.signal);
|
|
543
|
+
for (const entry of entries) {
|
|
544
|
+
const sourceStat = await lstatAsync(entry.temporary, { bigint: true });
|
|
545
|
+
throwIfCancelled(options.signal);
|
|
546
|
+
await link(entry.temporary, entry.destination);
|
|
547
|
+
committed.push({
|
|
548
|
+
destination: entry.destination,
|
|
549
|
+
dev: sourceStat.dev,
|
|
550
|
+
ino: sourceStat.ino
|
|
551
|
+
});
|
|
552
|
+
throwIfCancelled(options.signal);
|
|
553
|
+
}
|
|
554
|
+
await options.verifyTarget?.();
|
|
555
|
+
throwIfCancelled(options.signal);
|
|
556
|
+
}
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const rollbackErrors = [];
|
|
559
|
+
if (force) {
|
|
560
|
+
for (const item of published.reverse()) {
|
|
561
|
+
try {
|
|
562
|
+
if (destinationKind(item.destination) === "missing") continue;
|
|
563
|
+
const destinationStat = await lstatAsync(item.destination, { bigint: true });
|
|
564
|
+
if (!destinationStat.isFile() || destinationStat.isSymbolicLink() || destinationStat.dev !== item.dev || destinationStat.ino !== item.ino) {
|
|
565
|
+
throw new Error("scaffold destination changed during rollback");
|
|
566
|
+
}
|
|
567
|
+
await rm(item.destination, { force: true });
|
|
568
|
+
} catch (rollbackError) {
|
|
569
|
+
rollbackErrors.push(rollbackError);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
for (const item of backups.reverse()) {
|
|
573
|
+
try {
|
|
574
|
+
if (destinationKind(item.destination) !== "missing") {
|
|
575
|
+
throw new Error("scaffold destination changed before backup restore");
|
|
576
|
+
}
|
|
577
|
+
await rename(item.backup, item.destination);
|
|
578
|
+
} catch (rollbackError) {
|
|
579
|
+
rollbackErrors.push(rollbackError);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (rollbackErrors.length === 0 && backupDirectory !== void 0) {
|
|
583
|
+
try {
|
|
584
|
+
await rm(backupDirectory, { recursive: true, force: true });
|
|
585
|
+
backupDirectory = void 0;
|
|
586
|
+
} catch (rollbackError) {
|
|
587
|
+
rollbackErrors.push(rollbackError);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
for (const item of committed) {
|
|
592
|
+
try {
|
|
593
|
+
const destinationStat = await lstatAsync(item.destination, { bigint: true });
|
|
594
|
+
if (destinationStat.isFile() && !destinationStat.isSymbolicLink() && destinationStat.dev === item.dev && destinationStat.ino === item.ino) {
|
|
595
|
+
await rm(item.destination, { force: true });
|
|
596
|
+
}
|
|
597
|
+
} catch (cleanupError) {
|
|
598
|
+
if (cleanupError.code !== "ENOENT") {
|
|
599
|
+
rollbackErrors.push(cleanupError);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (rollbackErrors.length > 0) {
|
|
605
|
+
if (backupDirectory !== void 0) {
|
|
606
|
+
throw new ScaffoldRollbackError(
|
|
607
|
+
basename(backupDirectory),
|
|
608
|
+
new AggregateError([error, ...rollbackErrors])
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
throw new AggregateError([error, ...rollbackErrors], "scaffold publication rollback failed");
|
|
612
|
+
}
|
|
613
|
+
throw error;
|
|
614
|
+
} finally {
|
|
615
|
+
for (const entry of entries) await rm(entry.temporary, { force: true }).catch(() => void 0);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async function init(rest, opts) {
|
|
619
|
+
const log = opts.log ?? ((message) => console.log(message));
|
|
620
|
+
const err = opts.err ?? ((message) => console.error(message));
|
|
621
|
+
let args;
|
|
622
|
+
try {
|
|
623
|
+
args = parseArgs(rest);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
if (error instanceof CliArgError) {
|
|
626
|
+
err(`Error: ${error.message}`);
|
|
627
|
+
err("usage: kaji init [path] [--provider mock|openai|anthropic] [--yes] [--force]");
|
|
628
|
+
return 2;
|
|
629
|
+
}
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
let files;
|
|
633
|
+
try {
|
|
634
|
+
files = scaffoldFiles(args.provider);
|
|
635
|
+
} catch {
|
|
636
|
+
err("kaji init could not read installed package metadata");
|
|
637
|
+
return 1;
|
|
638
|
+
}
|
|
639
|
+
const destinations = Object.keys(files).map((name) => join(args.out, name));
|
|
640
|
+
const targetKind = destinationKind(args.out);
|
|
641
|
+
const kinds = destinations.map((path) => [path, destinationKind(path)]);
|
|
642
|
+
if (targetKind === "symlink" || kinds.some(([, kind]) => kind === "symlink")) {
|
|
643
|
+
err("unsafe scaffold destination: symbolic links are not allowed");
|
|
644
|
+
return 1;
|
|
645
|
+
}
|
|
646
|
+
const conflicts = kinds.filter(([, kind]) => kind !== "missing").map(([path]) => path);
|
|
647
|
+
if (conflicts.length > 0 && !args.force) {
|
|
648
|
+
err(
|
|
649
|
+
`refusing to overwrite without --force: ${conflicts.map((path) => path.split("/").at(-1)).join(", ")}`
|
|
650
|
+
);
|
|
651
|
+
return 1;
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
if (!existsSync(args.out)) mkdirSync(args.out, { recursive: true });
|
|
655
|
+
await (opts.initWorkerRunner ?? runPinnedInitWorker)(args.out, files, args.force);
|
|
656
|
+
for (const name of Object.keys(files)) log(`wrote ${join(args.out, name)}`);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (error instanceof ScaffoldRollbackError) {
|
|
659
|
+
err(
|
|
660
|
+
`kaji init failed while writing the scaffold; original preserved in target directory as ${error.recoveryDirectory}`
|
|
661
|
+
);
|
|
662
|
+
} else if (error instanceof Error && error.message.includes("unsafe scaffold destination")) {
|
|
663
|
+
err(error.message);
|
|
664
|
+
} else if (error.code === "EEXIST") {
|
|
665
|
+
err("refusing to overwrite without --force: destination changed during write");
|
|
666
|
+
} else {
|
|
667
|
+
err("kaji init failed while writing the scaffold");
|
|
668
|
+
}
|
|
669
|
+
return 1;
|
|
670
|
+
}
|
|
671
|
+
log("");
|
|
672
|
+
log(`Next: cd ${args.out} && npm install && npm start (or bun install && bun start)`);
|
|
673
|
+
return 0;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export {
|
|
677
|
+
scaffoldRecoveryDirectory,
|
|
678
|
+
runInitWorkerMain,
|
|
679
|
+
init
|
|
680
|
+
};
|
|
681
|
+
//# sourceMappingURL=chunk-SEBX54TR.js.map
|