@datagusto/cli 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +186 -2
- package/bin/run.js +5 -0
- package/dist/adapters/claude-code.d.ts +48 -0
- package/dist/adapters/claude-code.js +526 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/claude-settings.d.ts +29 -0
- package/dist/adapters/claude-settings.js +115 -0
- package/dist/adapters/claude-settings.js.map +1 -0
- package/dist/adapters/index.d.ts +10 -0
- package/dist/adapters/index.js +37 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/kiro-agent.d.ts +29 -0
- package/dist/adapters/kiro-agent.js +124 -0
- package/dist/adapters/kiro-agent.js.map +1 -0
- package/dist/adapters/kiro.d.ts +24 -0
- package/dist/adapters/kiro.js +752 -0
- package/dist/adapters/kiro.js.map +1 -0
- package/dist/adapters/types.d.ts +81 -0
- package/dist/adapters/types.js +14 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/api/client.d.ts +36 -0
- package/dist/api/client.js +85 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/errors.d.ts +4 -0
- package/dist/api/errors.js +25 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/commands/remove.d.ts +13 -0
- package/dist/commands/remove.js +59 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/setup.d.ts +16 -0
- package/dist/commands/setup.js +114 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +53 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/display.d.ts +2 -0
- package/dist/display.js +12 -0
- package/dist/display.js.map +1 -0
- package/dist/fs/atomic.d.ts +23 -0
- package/dist/fs/atomic.js +65 -0
- package/dist/fs/atomic.js.map +1 -0
- package/dist/key-input.d.ts +24 -0
- package/dist/key-input.js +96 -0
- package/dist/key-input.js.map +1 -0
- package/dist/platform.d.ts +24 -0
- package/dist/platform.js +61 -0
- package/dist/platform.js.map +1 -0
- package/dist/state/installs.d.ts +24 -0
- package/dist/state/installs.js +110 -0
- package/dist/state/installs.js.map +1 -0
- package/dist/vendor.d.ts +16 -0
- package/dist/vendor.js +60 -0
- package/dist/vendor.js.map +1 -0
- package/package.json +57 -4
- package/vendor/claude_code/.claude-plugin/plugin.json +55 -0
- package/vendor/claude_code/README.md +267 -0
- package/vendor/claude_code/dist/api-client.js +82 -0
- package/vendor/claude_code/dist/builtin-tools.generated.js +33 -0
- package/vendor/claude_code/dist/extractors.js +201 -0
- package/vendor/claude_code/dist/index.js +695 -0
- package/vendor/claude_code/dist/monitoring.js +66 -0
- package/vendor/claude_code/dist/state.js +71 -0
- package/vendor/claude_code/dist/types.js +5 -0
- package/vendor/claude_code/hooks/hooks.json +25 -0
- package/vendor/claude_code/package.json +26 -0
- package/vendor/kiro/.kiro/agents/kiro_default.json +20 -0
- package/vendor/kiro/.kiro/hooks/datagusto-post-tool-use.json +10 -0
- package/vendor/kiro/.kiro/hooks/datagusto-pre-tool-use.json +10 -0
- package/vendor/kiro/.kiro/hooks/datagusto-session-start.json +10 -0
- package/vendor/kiro/.kiro/hooks/datagusto-stop.json +10 -0
- package/vendor/kiro/.kiro/hooks/datagusto-user-prompt-submit.json +10 -0
- package/vendor/kiro/dist/api-client.js +82 -0
- package/vendor/kiro/dist/builtin-tools.generated.js +87 -0
- package/vendor/kiro/dist/engine.js +65 -0
- package/vendor/kiro/dist/extractors.js +201 -0
- package/vendor/kiro/dist/index.js +735 -0
- package/vendor/kiro/dist/monitoring.js +66 -0
- package/vendor/kiro/dist/state.js +71 -0
- package/vendor/kiro/dist/types.js +5 -0
- package/vendor/kiro/package.json +25 -0
- package/vendor/manifest.json +42 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// dg CLI - Kiro adapter
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// Installs the bundled kiro plugin into one Kiro workspace:
|
|
6
|
+
//
|
|
7
|
+
// <workspace>/
|
|
8
|
+
// dist/*.js, dist/package.json the plugin
|
|
9
|
+
// kiro.json serverUrl + apiKey, mode 0600
|
|
10
|
+
// .kiro/hooks/datagusto-*.json the plugin's five hooks, for `kiro-cli --v3`
|
|
11
|
+
// .kiro/agents/kiro_default.json the same five hooks, for the default engine
|
|
12
|
+
// .kiro/.datagusto-install.json ownership marker + file hashes
|
|
13
|
+
//
|
|
14
|
+
// This is the layout the plugin already expects: its hook files run
|
|
15
|
+
// `node dist/index.js` relative to the workspace, and its loadConfig() reads
|
|
16
|
+
// kiro.json from the folder above dist/. So the hook files are installed as
|
|
17
|
+
// they are and the plugin runtime is unchanged (ADR-0003 TR-2, TR-8). The
|
|
18
|
+
// plugin's package.json goes inside dist/, so node loads dist/*.js as ESM
|
|
19
|
+
// whatever the workspace's own package.json says.
|
|
20
|
+
//
|
|
21
|
+
// Kiro CLI 2.x has two engines and they find hooks in different places.
|
|
22
|
+
// `--v3` reads the standalone files in .kiro/hooks/. The default engine
|
|
23
|
+
// ignores those and runs the hooks in the config of the session's agent; a
|
|
24
|
+
// workspace agent file named kiro_default takes the built-in default agent's
|
|
25
|
+
// place, so a plain `kiro-cli` runs the hooks in it. `--v3` does not run the
|
|
26
|
+
// hooks in that file, so the two never fire together. If the workspace
|
|
27
|
+
// already has a kiro_default.json it is the user's: only the plugin's hook
|
|
28
|
+
// entries are merged in, and taken out again by clear (see kiro-agent.ts).
|
|
29
|
+
//
|
|
30
|
+
// There is a third engine, V1, which runs when the Kiro setting
|
|
31
|
+
// chat.agentEngine is "v1" or, per `kiro-cli chat --help`, when a session is
|
|
32
|
+
// started with `--agent-engine v1` (the flag was not tried). It does not let
|
|
33
|
+
// a workspace kiro_default replace the built-in agent and does not read
|
|
34
|
+
// .kiro/hooks/, so nothing dg installs runs under it. dg cannot fix that from
|
|
35
|
+
// the workspace; doctor reads the setting and reports it with the command
|
|
36
|
+
// that switches the workspace back. doctor can only see the setting: a flag
|
|
37
|
+
// passed on the command line leaves no trace, so a `--agent-engine v1`
|
|
38
|
+
// session is unchecked while every check passes. The default-engine check's
|
|
39
|
+
// own text says so.
|
|
40
|
+
//
|
|
41
|
+
// The workspace belongs to the user (TR-5), and unlike Claude Code there is
|
|
42
|
+
// no folder that is only ours. Ownership is therefore per file: the marker
|
|
43
|
+
// lists every file dg installed with its hash. A file at one of our paths
|
|
44
|
+
// that the marker doesn't list is a conflict, and setup stops before writing
|
|
45
|
+
// anything. clear() removes only files that still match their hash, so a
|
|
46
|
+
// build that has since written its own dist/index.js is left alone.
|
|
47
|
+
//
|
|
48
|
+
// Every new file is first written next to its target, and only then are
|
|
49
|
+
// they renamed into place, hook files and then the agent config last. A
|
|
50
|
+
// setup that fails part-way leaves the previous install (or nothing) behind.
|
|
51
|
+
//
|
|
52
|
+
// Verified on Kiro CLI 2.27 only. The Kiro IDE reads the same hook files, but
|
|
53
|
+
// whether it runs them the same way is unconfirmed; doctor says so.
|
|
54
|
+
import { execFile } from "node:child_process";
|
|
55
|
+
import { randomBytes } from "node:crypto";
|
|
56
|
+
import { access, constants, copyFile, lstat, mkdir, readFile, rename, rm, rmdir, stat } from "node:fs/promises";
|
|
57
|
+
import { basename, dirname, join, posix, relative, sep } from "node:path";
|
|
58
|
+
import { promisify } from "node:util";
|
|
59
|
+
import { DatagustoApiClient } from "../api/client.js";
|
|
60
|
+
import { explainApiError } from "../api/errors.js";
|
|
61
|
+
import { tildify } from "../display.js";
|
|
62
|
+
import { renameWithRetry, writeFileAtomic } from "../fs/atomic.js";
|
|
63
|
+
import { hasUnixPermissions, isWindows, onPath } from "../platform.js";
|
|
64
|
+
import { changedFiles, hashFile, loadVendoredPlugin, VendorError } from "../vendor.js";
|
|
65
|
+
import { addAgentHooks, agentHooks, eventsWithAgentHooks, hashText, readAgentFile, removeAgentHooks, serializeAgent, writeAgentFile, } from "./kiro-agent.js";
|
|
66
|
+
import { AdapterError, } from "./types.js";
|
|
67
|
+
const VENDOR_NAME = "kiro";
|
|
68
|
+
const CONFIG_FILE = "kiro.json";
|
|
69
|
+
const HOOKS_DIR = ".kiro/hooks";
|
|
70
|
+
const MARKER_FILE = ".kiro/.datagusto-install.json";
|
|
71
|
+
const INSTALLED_BY = "@datagusto/cli";
|
|
72
|
+
/** The default engine's hooks: a workspace agent that stands in for the built-in default. */
|
|
73
|
+
const DEFAULT_AGENT = "kiro_default";
|
|
74
|
+
const AGENT_CONFIG = `.kiro/agents/${DEFAULT_AGENT}.json`;
|
|
75
|
+
/** Kiro's own settings, workspace then user. Only ever read. */
|
|
76
|
+
const KIRO_SETTINGS = ".kiro/settings/cli.json";
|
|
77
|
+
const DEFAULT_AGENT_SETTING = "chat.defaultAgent";
|
|
78
|
+
/** Which engine a plain `kiro-cli` runs: "v1", "v2" (Kiro's default) or "v3". */
|
|
79
|
+
const AGENT_ENGINE_SETTING = "chat.agentEngine";
|
|
80
|
+
/** The engine that runs none of the hooks dg installs. */
|
|
81
|
+
const UNSUPPORTED_ENGINE = "v1";
|
|
82
|
+
const DEFAULT_ENGINE = "v2";
|
|
83
|
+
const KIRO_CLI_INSTALL = "curl -fsSL https://cli.kiro.dev/install | bash";
|
|
84
|
+
/** How to install the Kiro CLI on this operating system. */
|
|
85
|
+
function kiroInstallHint(platform) {
|
|
86
|
+
if (isWindows(platform))
|
|
87
|
+
return "the Windows installer from kiro.dev";
|
|
88
|
+
const viaBrew = (platform ?? process.platform) === "darwin" ? " (or `brew install --cask kiro-cli`)" : "";
|
|
89
|
+
return `\`${KIRO_CLI_INSTALL}\`${viaBrew}`;
|
|
90
|
+
}
|
|
91
|
+
// On the default engine hooks belong to an agent, so only sessions running the default agent get them.
|
|
92
|
+
const AGENT_CAVEAT = "On the default engine only the default agent runs the hooks: a session started with `--agent <name>` " +
|
|
93
|
+
"or switched with `/agent` to another agent is not protected. Neither is one started with `--agent-engine v1`, " +
|
|
94
|
+
"which dg can't detect.";
|
|
95
|
+
const IDE_CAVEAT = "Kiro IDE is not verified: use the Kiro CLI (`kiro-cli`) in this workspace.";
|
|
96
|
+
const UNPROTECTED = "a plain `kiro-cli` session in this workspace is not protected";
|
|
97
|
+
/** True for a hook entry that runs the plugin: every hook dg installs uses this command. */
|
|
98
|
+
const PLUGIN_COMMAND = "node dist/index.js";
|
|
99
|
+
const runsPlugin = (command) => command.trim() === PLUGIN_COMMAND;
|
|
100
|
+
export class KiroAdapter {
|
|
101
|
+
name = "kiro";
|
|
102
|
+
displayName = "Kiro";
|
|
103
|
+
perWorkspace = true;
|
|
104
|
+
whoami;
|
|
105
|
+
gitIgnores;
|
|
106
|
+
constructor(deps = {}) {
|
|
107
|
+
this.whoami = deps.whoami ?? ((serverUrl, apiKey) => new DatagustoApiClient({ serverUrl, apiKey }).whoami());
|
|
108
|
+
this.gitIgnores = deps.gitIgnores ?? runGitCheckIgnore;
|
|
109
|
+
}
|
|
110
|
+
async detect(ctx) {
|
|
111
|
+
const ws = workspaceOf(ctx);
|
|
112
|
+
if (!(await isDir(ws))) {
|
|
113
|
+
return {
|
|
114
|
+
found: false,
|
|
115
|
+
detail: `${ws} isn't a folder. Run this from your Kiro workspace, or pass it with --workspace <dir>.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// The Kiro IDE (`kiro`, ~/.kiro) isn't enough: the hooks are only verified on the CLI.
|
|
119
|
+
if (await onPath("kiro-cli", ctx.env, ctx.platform))
|
|
120
|
+
return { found: true };
|
|
121
|
+
return {
|
|
122
|
+
found: false,
|
|
123
|
+
detail: "The Kiro CLI isn't installed: there's no `kiro-cli` command on your PATH. Datagusto's hooks run in " +
|
|
124
|
+
"the Kiro CLI, and the Kiro IDE on its own is not enough. Install it with " +
|
|
125
|
+
kiroInstallHint(ctx.platform) +
|
|
126
|
+
", sign in with `kiro-cli login`, open a new terminal, then run this again.",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async preflight(ctx) {
|
|
130
|
+
const ws = workspaceOf(ctx);
|
|
131
|
+
const plugin = await loadVendoredPlugin(ctx.vendorDir, VENDOR_NAME);
|
|
132
|
+
const owner = await ownership(ws);
|
|
133
|
+
await refuseConflicts(ws, installedFiles(plugin), owner, ctx.homeDir);
|
|
134
|
+
// Read now so a broken agent config stops setup before the key is asked for.
|
|
135
|
+
await planAgentConfig(ws, join(ctx.vendorDir, VENDOR_NAME), plugin, owner);
|
|
136
|
+
}
|
|
137
|
+
async plan(ctx, _conn) {
|
|
138
|
+
const ws = workspaceOf(ctx);
|
|
139
|
+
const plugin = await loadVendoredPlugin(ctx.vendorDir, VENDOR_NAME);
|
|
140
|
+
const files = installedFiles(plugin);
|
|
141
|
+
const owner = await ownership(ws);
|
|
142
|
+
await refuseConflicts(ws, files, owner, ctx.homeDir);
|
|
143
|
+
const update = owner.state === "ours";
|
|
144
|
+
const agent = await planAgentConfig(ws, join(ctx.vendorDir, VENDOR_NAME), plugin, owner);
|
|
145
|
+
const changes = [
|
|
146
|
+
{
|
|
147
|
+
kind: "copy-dir",
|
|
148
|
+
path: join(ws, "dist"),
|
|
149
|
+
description: `${update ? "Update" : "Install"} the Datagusto safety plugin v${plugin.pluginVersion}`,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
kind: update ? "update-file" : "create-file",
|
|
153
|
+
path: join(ws, CONFIG_FILE),
|
|
154
|
+
description: "Save the server URL and API key (readable only by you)",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
kind: "copy-dir",
|
|
158
|
+
path: join(ws, ...HOOKS_DIR.split("/")),
|
|
159
|
+
description: `Add the plugin's ${hookFiles(files).length} hooks for \`kiro-cli --v3\` (your other hooks are left as they are)`,
|
|
160
|
+
},
|
|
161
|
+
agent.action === "merge"
|
|
162
|
+
? {
|
|
163
|
+
kind: "merge-json",
|
|
164
|
+
path: agent.file.path,
|
|
165
|
+
description: `Add the plugin's ${agent.events} hooks to your default agent, for plain \`kiro-cli\` (the rest of the file is left as it is)`,
|
|
166
|
+
}
|
|
167
|
+
: {
|
|
168
|
+
kind: agent.action === "create" ? "create-file" : "update-file",
|
|
169
|
+
path: agent.file.path,
|
|
170
|
+
description: `${agent.action === "create" ? "Add" : "Update"} the default agent that runs the plugin's ${agent.events} hooks, for plain \`kiro-cli\``,
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
return { agent: this.name, changes };
|
|
174
|
+
}
|
|
175
|
+
async apply(ctx, _plan, conn) {
|
|
176
|
+
const ws = workspaceOf(ctx);
|
|
177
|
+
const src = join(ctx.vendorDir, VENDOR_NAME);
|
|
178
|
+
const plugin = await loadVendoredPlugin(ctx.vendorDir, VENDOR_NAME);
|
|
179
|
+
const damaged = await changedFiles(src, plugin.files);
|
|
180
|
+
if (damaged.length > 0) {
|
|
181
|
+
throw new AdapterError(`The plugin files bundled with dg are damaged (${damaged.join(", ")}). ` +
|
|
182
|
+
"Reinstall the CLI: npm install -g @datagusto/cli");
|
|
183
|
+
}
|
|
184
|
+
const files = installedFiles(plugin);
|
|
185
|
+
const owner = await ownership(ws);
|
|
186
|
+
await refuseConflicts(ws, files, owner, ctx.homeDir);
|
|
187
|
+
const update = owner.state === "ours";
|
|
188
|
+
const agent = await planAgentConfig(ws, src, plugin, owner);
|
|
189
|
+
// A re-run keeps any other options the user put in kiro.json
|
|
190
|
+
// (verbose, monitorOnly, …).
|
|
191
|
+
const previous = update ? await readJsonObject(join(ws, CONFIG_FILE)) : {};
|
|
192
|
+
const config = { ...previous, serverUrl: conn.serverUrl, apiKey: conn.apiKey };
|
|
193
|
+
const targets = [...Object.keys(files), CONFIG_FILE, MARKER_FILE, AGENT_CONFIG];
|
|
194
|
+
const at = (rel) => join(ws, ...rel.split("/"));
|
|
195
|
+
const suffix = randomBytes(4).toString("hex");
|
|
196
|
+
const createdDirs = [];
|
|
197
|
+
const staged = [];
|
|
198
|
+
const stage = async (rel, write) => {
|
|
199
|
+
const to = at(rel);
|
|
200
|
+
const tmp = join(dirname(to), `.${basename(to)}.${suffix}.tmp`);
|
|
201
|
+
staged.push({ tmp, to });
|
|
202
|
+
await write(tmp);
|
|
203
|
+
};
|
|
204
|
+
try {
|
|
205
|
+
for (const rel of targets)
|
|
206
|
+
await makeDirs(ws, posix.dirname(rel), createdDirs);
|
|
207
|
+
const marker = {
|
|
208
|
+
installedBy: INSTALLED_BY,
|
|
209
|
+
cliVersion: ctx.cliVersion,
|
|
210
|
+
pluginVersion: plugin.pluginVersion,
|
|
211
|
+
files,
|
|
212
|
+
createdDirs: [...new Set([...(update ? owner.marker.createdDirs : []), ...createdDirs])],
|
|
213
|
+
agentConfig: agent.marker,
|
|
214
|
+
};
|
|
215
|
+
// Hook files go last: Kiro must never see a hook whose script isn't there yet.
|
|
216
|
+
const hooks = hookFiles(files);
|
|
217
|
+
const copy = (rel) => stage(rel, (tmp) => copyFile(join(src, ...vendorPath(rel).split("/")), tmp));
|
|
218
|
+
for (const rel of Object.keys(files).filter((f) => !hooks.includes(f)))
|
|
219
|
+
await copy(rel);
|
|
220
|
+
await stage(CONFIG_FILE, (tmp) => writeFileAtomic(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 }));
|
|
221
|
+
await stage(MARKER_FILE, (tmp) => writeFileAtomic(tmp, JSON.stringify(marker, null, 2) + "\n"));
|
|
222
|
+
for (const rel of hooks)
|
|
223
|
+
await copy(rel);
|
|
224
|
+
for (const { tmp, to } of staged)
|
|
225
|
+
await renameWithRetry(tmp, to);
|
|
226
|
+
// The agent config goes last and in one step: it may be the user's own
|
|
227
|
+
// file, so there must be nothing left to fail once it has changed.
|
|
228
|
+
await writeAgentFile(agent.file, agent.text);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
for (const { tmp, to } of staged) {
|
|
232
|
+
await rm(tmp, { force: true });
|
|
233
|
+
// Nothing was here before a first install, so take back what got renamed.
|
|
234
|
+
if (!update)
|
|
235
|
+
await rm(to, { force: true });
|
|
236
|
+
}
|
|
237
|
+
await removeEmptyDirs(ws, createdDirs);
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
// Files an older plugin version had that this one doesn't.
|
|
241
|
+
if (update) {
|
|
242
|
+
for (const [rel, hash] of Object.entries(owner.marker.files)) {
|
|
243
|
+
if (!(rel in files) && (await hashOrUndefined(at(rel))) === hash)
|
|
244
|
+
await rm(at(rel), { force: true });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
agent: this.name,
|
|
249
|
+
workspace: ws,
|
|
250
|
+
agentId: conn.agentId,
|
|
251
|
+
paths: targets.map(at),
|
|
252
|
+
installedAt: new Date().toISOString(),
|
|
253
|
+
cliVersion: ctx.cliVersion,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
async clear(ctx, record) {
|
|
257
|
+
const result = { removed: [], edited: [], skipped: [] };
|
|
258
|
+
const ws = record.workspace ?? workspaceOf(ctx);
|
|
259
|
+
const markerPath = join(ws, ...MARKER_FILE.split("/"));
|
|
260
|
+
const owner = await ownership(ws);
|
|
261
|
+
if (owner.state !== "ours") {
|
|
262
|
+
// Without the marker, nothing here can be told from the user's own files.
|
|
263
|
+
for (const path of record.paths) {
|
|
264
|
+
if (path !== markerPath && (await exists(path)))
|
|
265
|
+
result.skipped.push(path);
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
const rel = (path) => relative(ws, path).split(sep).join("/");
|
|
270
|
+
const isHook = (path) => rel(path).startsWith(`${HOOKS_DIR}/`) || rel(path) === AGENT_CONFIG;
|
|
271
|
+
// Hooks first: a hook left pointing at a deleted dist/index.js would error on every call.
|
|
272
|
+
const paths = [...record.paths.filter(isHook), ...record.paths.filter((p) => !isHook(p) && p !== markerPath)];
|
|
273
|
+
for (const path of paths) {
|
|
274
|
+
if (!(await exists(path)))
|
|
275
|
+
continue;
|
|
276
|
+
if (rel(path) === AGENT_CONFIG) {
|
|
277
|
+
const outcome = await clearAgentConfig(path, owner.marker.agentConfig);
|
|
278
|
+
if (outcome)
|
|
279
|
+
result[outcome].push(path);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const hash = owner.marker.files[rel(path)];
|
|
283
|
+
const ours = rel(path) === CONFIG_FILE || (hash !== undefined && (await hashOrUndefined(path)) === hash);
|
|
284
|
+
if (ours) {
|
|
285
|
+
await rm(path, { force: true });
|
|
286
|
+
result.removed.push(path);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// Changed since setup, e.g. the workspace's own build wrote dist/index.js.
|
|
290
|
+
result.skipped.push(path);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
await rm(markerPath, { force: true });
|
|
294
|
+
result.removed.push(markerPath);
|
|
295
|
+
await removeEmptyDirs(ws, owner.marker.createdDirs);
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
async doctor(ctx, record) {
|
|
299
|
+
const report = { agent: this.name, checks: [] };
|
|
300
|
+
const add = (check) => report.checks.push(check);
|
|
301
|
+
const ws = record.workspace ?? workspaceOf(ctx);
|
|
302
|
+
const again = `Run \`dg setup kiro --workspace ${ws}\` again`;
|
|
303
|
+
add((await onPath("kiro-cli", ctx.env, ctx.platform))
|
|
304
|
+
? { name: "Kiro CLI", status: "ok", detail: `found on your PATH. ${IDE_CAVEAT}` }
|
|
305
|
+
: { name: "Kiro CLI", status: "warn", detail: `\`kiro-cli\` isn't on your PATH. Install it with ${kiroInstallHint(ctx.platform)}. ${IDE_CAVEAT}`,
|
|
306
|
+
});
|
|
307
|
+
const owner = await ownership(ws);
|
|
308
|
+
if (owner.state !== "ours") {
|
|
309
|
+
add({
|
|
310
|
+
name: "Plugin files",
|
|
311
|
+
status: "fail",
|
|
312
|
+
detail: `${tildify(ws, ctx.homeDir)} no longer has the install dg made. ${again} to reinstall it.`,
|
|
313
|
+
});
|
|
314
|
+
return report;
|
|
315
|
+
}
|
|
316
|
+
const changed = await changedFiles(ws, owner.marker.files);
|
|
317
|
+
const hooks = hookFiles(owner.marker.files);
|
|
318
|
+
const changedPlugin = changed.filter((f) => !hooks.includes(f));
|
|
319
|
+
const changedHooks = changed.filter((f) => hooks.includes(f));
|
|
320
|
+
add(changedPlugin.length === 0
|
|
321
|
+
? { name: "Plugin files", status: "ok", detail: `${tildify(join(ws, "dist"), ctx.homeDir)} (v${owner.marker.pluginVersion})` }
|
|
322
|
+
: {
|
|
323
|
+
name: "Plugin files",
|
|
324
|
+
status: "fail",
|
|
325
|
+
detail: `Changed or missing: ${changedPlugin.join(", ")}. ${again} to repair them.`,
|
|
326
|
+
});
|
|
327
|
+
add(changedHooks.length === 0
|
|
328
|
+
? { name: V3_HOOKS, status: "ok", detail: `${hooks.length} hook files in ${HOOKS_DIR}` }
|
|
329
|
+
: {
|
|
330
|
+
name: V3_HOOKS,
|
|
331
|
+
status: "fail",
|
|
332
|
+
detail: `Changed or missing: ${changedHooks.join(", ")}, so a \`kiro-cli --v3\` session is not fully protected. ${again} to repair them.`,
|
|
333
|
+
});
|
|
334
|
+
for (const check of await defaultEngineChecks(ws, ctx.homeDir, owner.marker, again))
|
|
335
|
+
add(check);
|
|
336
|
+
for (const check of await agentEngineChecks(ws, ctx.homeDir))
|
|
337
|
+
add(check);
|
|
338
|
+
try {
|
|
339
|
+
const bundled = installedFiles(await loadVendoredPlugin(ctx.vendorDir, VENDOR_NAME));
|
|
340
|
+
if (!sameFiles(bundled, owner.marker.files)) {
|
|
341
|
+
add({
|
|
342
|
+
name: "Plugin version",
|
|
343
|
+
status: "warn",
|
|
344
|
+
detail: `This dg bundles a different plugin build than the v${owner.marker.pluginVersion} installed here. ${again} to update it.`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// A broken CLI package is setup's problem; status still reports the rest.
|
|
350
|
+
}
|
|
351
|
+
const configPath = join(ws, CONFIG_FILE);
|
|
352
|
+
const config = await readJsonObject(configPath);
|
|
353
|
+
const serverUrl = typeof config.serverUrl === "string" ? config.serverUrl : "";
|
|
354
|
+
const apiKey = typeof config.apiKey === "string" ? config.apiKey : "";
|
|
355
|
+
if (!serverUrl || !apiKey) {
|
|
356
|
+
add({
|
|
357
|
+
name: "Config",
|
|
358
|
+
status: "fail",
|
|
359
|
+
detail: `${tildify(configPath, ctx.homeDir)} has no server URL or API key. ${again}.`,
|
|
360
|
+
});
|
|
361
|
+
return report;
|
|
362
|
+
}
|
|
363
|
+
// Windows has no mode bits to check: the user profile's access rules keep the file private.
|
|
364
|
+
const fileMode = hasUnixPermissions(ctx.platform) ? (await stat(configPath)).mode & 0o777 : 0o600;
|
|
365
|
+
if (fileMode & 0o077) {
|
|
366
|
+
add({
|
|
367
|
+
name: "Config",
|
|
368
|
+
status: "warn",
|
|
369
|
+
detail: `${tildify(configPath, ctx.homeDir)} holds your API key but other users can read it. Run: chmod 600 ${configPath}`,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
else if ((await this.gitIgnores(ws, CONFIG_FILE, ctx.env)) === false) {
|
|
373
|
+
add({
|
|
374
|
+
name: "Config",
|
|
375
|
+
status: "warn",
|
|
376
|
+
detail: `${CONFIG_FILE} holds your API key and git isn't ignoring it. Add ${CONFIG_FILE} to this workspace's .gitignore.`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
add({ name: "Config", status: "ok", detail: serverUrl });
|
|
381
|
+
}
|
|
382
|
+
const who = await this.whoami(serverUrl, apiKey);
|
|
383
|
+
if (!who.ok) {
|
|
384
|
+
add({ name: "API key", status: "fail", detail: explainApiError(who, serverUrl) });
|
|
385
|
+
return report;
|
|
386
|
+
}
|
|
387
|
+
report.agentName = who.data.name;
|
|
388
|
+
add(who.data.agent_id === record.agentId
|
|
389
|
+
? { name: "API key", status: "ok", detail: `agent "${who.data.name}"` }
|
|
390
|
+
: {
|
|
391
|
+
name: "API key",
|
|
392
|
+
status: "warn",
|
|
393
|
+
detail: `The key now belongs to agent "${who.data.name}", not the one it was set up with. ` +
|
|
394
|
+
`${again} if that's not what you want.`,
|
|
395
|
+
});
|
|
396
|
+
return report;
|
|
397
|
+
}
|
|
398
|
+
async installedServerUrl(ctx) {
|
|
399
|
+
const ws = workspaceOf(ctx);
|
|
400
|
+
if ((await ownership(ws)).state !== "ours")
|
|
401
|
+
return undefined;
|
|
402
|
+
const config = await readJsonObject(join(ws, CONFIG_FILE));
|
|
403
|
+
return typeof config.serverUrl === "string" && config.serverUrl ? config.serverUrl : undefined;
|
|
404
|
+
}
|
|
405
|
+
nextSteps(ctx) {
|
|
406
|
+
const ws = workspaceOf(ctx);
|
|
407
|
+
return [
|
|
408
|
+
`Start \`kiro-cli\` in ${tildify(ws, ctx.homeDir)} to begin sending safety events: the hooks only run for sessions opened in that folder.`,
|
|
409
|
+
`Both engines are covered: plain \`kiro-cli\` through ${AGENT_CONFIG}, and \`kiro-cli --v3\` through ${HOOKS_DIR}/.`,
|
|
410
|
+
AGENT_CAVEAT,
|
|
411
|
+
IDE_CAVEAT,
|
|
412
|
+
`${CONFIG_FILE} in that folder holds your API key. Keep it out of git.`,
|
|
413
|
+
];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// -- Internal helpers ---------------------------------------------------------
|
|
417
|
+
function workspaceOf(ctx) {
|
|
418
|
+
if (!ctx.workspace)
|
|
419
|
+
throw new AdapterError("Kiro is set up one workspace at a time. Pass it with --workspace <dir>.");
|
|
420
|
+
return ctx.workspace;
|
|
421
|
+
}
|
|
422
|
+
/** Where a bundled file goes in the workspace. Only package.json moves. */
|
|
423
|
+
function installPath(rel) {
|
|
424
|
+
return rel === "package.json" ? "dist/package.json" : rel;
|
|
425
|
+
}
|
|
426
|
+
function vendorPath(rel) {
|
|
427
|
+
return rel === "dist/package.json" ? "package.json" : rel;
|
|
428
|
+
}
|
|
429
|
+
/** The bundled files keyed by where they are installed, with their hashes. */
|
|
430
|
+
function installedFiles(plugin) {
|
|
431
|
+
return Object.fromEntries(Object.entries(plugin.files)
|
|
432
|
+
// The agent config is merged, not copied: it can be the user's own file.
|
|
433
|
+
.filter(([rel]) => rel !== AGENT_CONFIG)
|
|
434
|
+
.map(([rel, hash]) => [installPath(rel), hash]));
|
|
435
|
+
}
|
|
436
|
+
const V3_HOOKS = "Hooks (--v3)";
|
|
437
|
+
const DEFAULT_HOOKS = "Hooks (default engine)";
|
|
438
|
+
/** The default engine's events the plugin has to be on. */
|
|
439
|
+
const DEFAULT_ENGINE_EVENTS = ["agentSpawn", "userPromptSubmit", "preToolUse", "postToolUse", "stop"];
|
|
440
|
+
/** The agent config dg bundles: the default agent plus the plugin's hooks. */
|
|
441
|
+
async function bundledAgentConfig(vendorSrc, plugin) {
|
|
442
|
+
if (!(AGENT_CONFIG in plugin.files)) {
|
|
443
|
+
throw new VendorError(`This copy of dg doesn't include the Kiro agent config (${AGENT_CONFIG}). Reinstall the CLI: npm install -g @datagusto/cli`);
|
|
444
|
+
}
|
|
445
|
+
return readJsonObject(join(vendorSrc, ...AGENT_CONFIG.split("/")));
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Work out the agent config setup will write. A file dg created and nobody
|
|
449
|
+
* has edited since is replaced with the bundled one; anything else that is
|
|
450
|
+
* already there is the user's, and only gets the plugin's hook entries.
|
|
451
|
+
*/
|
|
452
|
+
async function planAgentConfig(ws, vendorSrc, plugin, owner) {
|
|
453
|
+
const bundled = await bundledAgentConfig(vendorSrc, plugin);
|
|
454
|
+
const hooks = agentHooks(bundled, AGENT_CONFIG);
|
|
455
|
+
const file = await readAgentFile(join(ws, ...AGENT_CONFIG.split("/")));
|
|
456
|
+
const previous = owner.state === "ours" ? owner.marker.agentConfig : undefined;
|
|
457
|
+
let action;
|
|
458
|
+
let data;
|
|
459
|
+
if (!file.exists) {
|
|
460
|
+
action = "create";
|
|
461
|
+
data = bundled;
|
|
462
|
+
}
|
|
463
|
+
else if (previous?.created && file.hash === previous.hash) {
|
|
464
|
+
action = "replace";
|
|
465
|
+
data = bundled;
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
action = "merge";
|
|
469
|
+
data = file.data;
|
|
470
|
+
removeAgentHooks(data, runsPlugin, file.path);
|
|
471
|
+
addAgentHooks(data, hooks, file.path);
|
|
472
|
+
}
|
|
473
|
+
const text = serializeAgent(data);
|
|
474
|
+
return {
|
|
475
|
+
file,
|
|
476
|
+
action,
|
|
477
|
+
text,
|
|
478
|
+
// Once the user's content is in the file it is never dg's to delete.
|
|
479
|
+
marker: { path: AGENT_CONFIG, created: action !== "merge", hash: hashText(text) },
|
|
480
|
+
events: Object.keys(hooks).length,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Undo the agent config: delete it if dg created it and it hasn't changed,
|
|
485
|
+
* otherwise take only the plugin's hook entries back out.
|
|
486
|
+
*/
|
|
487
|
+
async function clearAgentConfig(path, marker) {
|
|
488
|
+
if (!marker)
|
|
489
|
+
return "skipped";
|
|
490
|
+
let file;
|
|
491
|
+
try {
|
|
492
|
+
file = await readAgentFile(path);
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
if (err instanceof AdapterError)
|
|
496
|
+
return "skipped";
|
|
497
|
+
throw err;
|
|
498
|
+
}
|
|
499
|
+
if (!file.exists)
|
|
500
|
+
return undefined;
|
|
501
|
+
if (marker.created && file.hash === marker.hash) {
|
|
502
|
+
await rm(file.path, { force: true });
|
|
503
|
+
return "removed";
|
|
504
|
+
}
|
|
505
|
+
if (removeAgentHooks(file.data, runsPlugin, path) === 0)
|
|
506
|
+
return undefined;
|
|
507
|
+
await writeAgentFile(file, serializeAgent(file.data));
|
|
508
|
+
return "edited";
|
|
509
|
+
}
|
|
510
|
+
/** Whether a plain `kiro-cli` session in `ws` runs the plugin's hooks. */
|
|
511
|
+
async function defaultEngineChecks(ws, homeDir, marker, again) {
|
|
512
|
+
const path = join(ws, ...AGENT_CONFIG.split("/"));
|
|
513
|
+
const fail = (detail) => [{ name: DEFAULT_HOOKS, status: "fail", detail }];
|
|
514
|
+
if (!marker.agentConfig) {
|
|
515
|
+
return fail(`This install only covers \`kiro-cli --v3\`: ${UNPROTECTED}. ${again} to add the default engine's hooks.`);
|
|
516
|
+
}
|
|
517
|
+
let file;
|
|
518
|
+
try {
|
|
519
|
+
file = await readAgentFile(path);
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
if (!(err instanceof AdapterError))
|
|
523
|
+
throw err;
|
|
524
|
+
return fail(`${AGENT_CONFIG} can't be read as an agent config, so ${UNPROTECTED}. Fix the file, then ${lower(again)}.`);
|
|
525
|
+
}
|
|
526
|
+
if (!file.exists)
|
|
527
|
+
return fail(`${AGENT_CONFIG} is missing, so ${UNPROTECTED}. ${again} to put it back.`);
|
|
528
|
+
const present = eventsWithAgentHooks(file.data, runsPlugin, path);
|
|
529
|
+
const missing = DEFAULT_ENGINE_EVENTS.filter((e) => !present.includes(e));
|
|
530
|
+
if (missing.length > 0) {
|
|
531
|
+
return fail(`${AGENT_CONFIG} no longer runs the plugin on ${missing.join(", ")}, so ${UNPROTECTED}. ${again} to repair it.`);
|
|
532
|
+
}
|
|
533
|
+
const checks = [
|
|
534
|
+
{
|
|
535
|
+
name: DEFAULT_HOOKS,
|
|
536
|
+
status: "ok",
|
|
537
|
+
detail: `${AGENT_CONFIG} runs the plugin on ${DEFAULT_ENGINE_EVENTS.length} events. ${AGENT_CAVEAT}`,
|
|
538
|
+
},
|
|
539
|
+
];
|
|
540
|
+
// Kiro starts the agent named by chat.defaultAgent instead; the workspace's setting wins over the user's.
|
|
541
|
+
for (const settings of [join(ws, ...KIRO_SETTINGS.split("/")), join(homeDir, ...KIRO_SETTINGS.split("/"))]) {
|
|
542
|
+
const chosen = (await readJsonObject(settings))[DEFAULT_AGENT_SETTING];
|
|
543
|
+
if (typeof chosen !== "string" || chosen === "")
|
|
544
|
+
continue;
|
|
545
|
+
if (chosen !== DEFAULT_AGENT) {
|
|
546
|
+
checks.push({
|
|
547
|
+
name: "Default agent",
|
|
548
|
+
status: "warn",
|
|
549
|
+
detail: `${tildify(settings, homeDir)} sets ${DEFAULT_AGENT_SETTING} to "${chosen}", so a plain \`kiro-cli\` here starts that agent ` +
|
|
550
|
+
`and not the one with the Datagusto hooks. Start sessions with \`kiro-cli --agent ${DEFAULT_AGENT}\` or \`kiro-cli --v3\`, ` +
|
|
551
|
+
`or make it the default for this workspace: kiro-cli settings --workspace ${DEFAULT_AGENT_SETTING} ${DEFAULT_AGENT}`,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
const userAgent = join(homeDir, ...AGENT_CONFIG.split("/"));
|
|
557
|
+
if (await exists(userAgent)) {
|
|
558
|
+
checks.push({
|
|
559
|
+
name: "Your own default agent",
|
|
560
|
+
status: "warn",
|
|
561
|
+
detail: `You also have ${tildify(userAgent, homeDir)}. In this workspace Kiro uses ${AGENT_CONFIG} instead, ` +
|
|
562
|
+
"so copy anything you rely on from your own file into it.",
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return checks;
|
|
566
|
+
}
|
|
567
|
+
/** The value of one of Kiro's own settings and the file that sets it; the workspace's wins over the user's. */
|
|
568
|
+
async function kiroSetting(ws, homeDir, key) {
|
|
569
|
+
for (const file of [join(ws, ...KIRO_SETTINGS.split("/")), join(homeDir, ...KIRO_SETTINGS.split("/"))]) {
|
|
570
|
+
const value = (await readJsonObject(file))[key];
|
|
571
|
+
if (typeof value === "string" && value !== "")
|
|
572
|
+
return { value, file };
|
|
573
|
+
}
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Whether Kiro's own settings put plain `kiro-cli` on the V1 engine, which
|
|
578
|
+
* runs none of the hooks dg installs. Only ever read: the engine is the
|
|
579
|
+
* user's choice to change.
|
|
580
|
+
*
|
|
581
|
+
* Severity follows the rule for every check here: fail when dg knows a plain
|
|
582
|
+
* session goes unchecked, warn when that depends on something dg doesn't
|
|
583
|
+
* manage. On V1 a plain session runs Kiro's built-in default agent, which has
|
|
584
|
+
* no hooks, so that is a failure. If chat.defaultAgent names another agent,
|
|
585
|
+
* the plain session runs that one, and whether it carries the Datagusto hooks
|
|
586
|
+
* is the user's business: a warning, as it is on the default engine.
|
|
587
|
+
*
|
|
588
|
+
* This sees the setting only. `kiro-cli --agent-engine v1` selects the same
|
|
589
|
+
* engine from the command line and cannot be detected (AGENT_CAVEAT says so).
|
|
590
|
+
*/
|
|
591
|
+
async function agentEngineChecks(ws, homeDir) {
|
|
592
|
+
const engine = await kiroSetting(ws, homeDir, AGENT_ENGINE_SETTING);
|
|
593
|
+
// Kiro documents the value as lowercase ("v1"). Whether it also accepts "V1"
|
|
594
|
+
// is unverified; matching it anyway costs nothing and errs towards warning.
|
|
595
|
+
if (!engine || engine.value.trim().toLowerCase() !== UNSUPPORTED_ENGINE)
|
|
596
|
+
return [];
|
|
597
|
+
const agent = await kiroSetting(ws, homeDir, DEFAULT_AGENT_SETTING);
|
|
598
|
+
const ownAgent = agent && agent.value !== DEFAULT_AGENT ? agent.value : undefined;
|
|
599
|
+
const intro = `${tildify(engine.file, homeDir)} sets ${AGENT_ENGINE_SETTING} to "${engine.value}". ` +
|
|
600
|
+
"Kiro's V1 engine doesn't run the Datagusto hooks that setup installs";
|
|
601
|
+
const fix = "`kiro-cli --v3` sessions here are still covered. " +
|
|
602
|
+
"Switch this workspace to Kiro's default engine by running this in the workspace: " +
|
|
603
|
+
`kiro-cli settings --workspace ${AGENT_ENGINE_SETTING} ${DEFAULT_ENGINE}`;
|
|
604
|
+
return [
|
|
605
|
+
ownAgent
|
|
606
|
+
? {
|
|
607
|
+
name: "Agent engine",
|
|
608
|
+
status: "warn",
|
|
609
|
+
detail: `${intro}. A plain \`kiro-cli\` here starts your agent "${ownAgent}", so its tool calls are checked ` +
|
|
610
|
+
`only if you added the Datagusto hooks to that agent yourself, which dg doesn't check. ${fix}`,
|
|
611
|
+
}
|
|
612
|
+
: {
|
|
613
|
+
name: "Agent engine",
|
|
614
|
+
status: "fail",
|
|
615
|
+
detail: `${intro}, so ${UNPROTECTED}: its tool calls run unchecked. ${fix}`,
|
|
616
|
+
},
|
|
617
|
+
];
|
|
618
|
+
}
|
|
619
|
+
function lower(sentence) {
|
|
620
|
+
return sentence.charAt(0).toLowerCase() + sentence.slice(1);
|
|
621
|
+
}
|
|
622
|
+
function hookFiles(files) {
|
|
623
|
+
return Object.keys(files).filter((f) => f.startsWith(`${HOOKS_DIR}/`));
|
|
624
|
+
}
|
|
625
|
+
/** Stop if a file dg would write is already there and isn't one dg installed. */
|
|
626
|
+
async function refuseConflicts(ws, files, owner, homeDir) {
|
|
627
|
+
const ours = new Set(owner.state === "ours" ? [...Object.keys(owner.marker.files), CONFIG_FILE, MARKER_FILE] : []);
|
|
628
|
+
const conflicts = [];
|
|
629
|
+
for (const rel of [...Object.keys(files), CONFIG_FILE, MARKER_FILE]) {
|
|
630
|
+
if (!ours.has(rel) && (await occupied(ws, rel)))
|
|
631
|
+
conflicts.push(rel);
|
|
632
|
+
}
|
|
633
|
+
if (conflicts.length > 0) {
|
|
634
|
+
throw new AdapterError(`${tildify(ws, homeDir)} already has ${conflicts.join(", ")}, which dg didn't put there, so nothing was changed. ` +
|
|
635
|
+
"The Kiro plugin needs those paths: move the files away or pick another --workspace, then run this again.");
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function ownership(ws) {
|
|
639
|
+
const marker = await readJsonObject(join(ws, ...MARKER_FILE.split("/")));
|
|
640
|
+
if (marker.installedBy !== INSTALLED_BY || typeof marker.files !== "object" || marker.files === null) {
|
|
641
|
+
return { state: "missing" };
|
|
642
|
+
}
|
|
643
|
+
const createdDirs = Array.isArray(marker.createdDirs) ? marker.createdDirs.filter((d) => typeof d === "string") : [];
|
|
644
|
+
return { state: "ours", marker: { ...marker, createdDirs } };
|
|
645
|
+
}
|
|
646
|
+
/** Create `relDir` under `ws` one level at a time, noting each folder made. */
|
|
647
|
+
async function makeDirs(ws, relDir, created) {
|
|
648
|
+
if (relDir === ".")
|
|
649
|
+
return;
|
|
650
|
+
const parts = relDir.split("/");
|
|
651
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
652
|
+
const rel = parts.slice(0, i).join("/");
|
|
653
|
+
try {
|
|
654
|
+
await mkdir(join(ws, ...parts.slice(0, i)), { mode: 0o755 });
|
|
655
|
+
created.push(rel);
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
if (err.code !== "EEXIST")
|
|
659
|
+
throw err;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
/** Remove `dirs` (deepest first) where they're empty; anything else stays. */
|
|
664
|
+
async function removeEmptyDirs(ws, dirs) {
|
|
665
|
+
for (const rel of [...dirs].sort((a, b) => b.length - a.length)) {
|
|
666
|
+
if (rel.split("/").some((p) => p === ".." || p === ""))
|
|
667
|
+
continue;
|
|
668
|
+
try {
|
|
669
|
+
await rmdir(join(ws, ...rel.split("/")));
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
// not empty, or already gone
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async function readJsonObject(path) {
|
|
677
|
+
try {
|
|
678
|
+
const v = JSON.parse(await readFile(path, "utf8"));
|
|
679
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
return {};
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function sameFiles(a, b) {
|
|
686
|
+
const keys = Object.keys(a);
|
|
687
|
+
return keys.length === Object.keys(b).length && keys.every((k) => a[k] === b[k]);
|
|
688
|
+
}
|
|
689
|
+
async function hashOrUndefined(path) {
|
|
690
|
+
try {
|
|
691
|
+
return await hashFile(path);
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async function exists(path) {
|
|
698
|
+
try {
|
|
699
|
+
await lstat(path);
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/** Like exists(), but a file where a parent folder should be also counts. */
|
|
707
|
+
/**
|
|
708
|
+
* True when something already sits at `rel` inside `ws`, or a file sits where
|
|
709
|
+
* one of its folders should be (a file called `dist` blocks `dist/index.js`).
|
|
710
|
+
* macOS and Linux report the second case as ENOTDIR; Windows says ENOENT, so
|
|
711
|
+
* the folders on the way are checked one by one.
|
|
712
|
+
*/
|
|
713
|
+
async function occupied(ws, rel) {
|
|
714
|
+
const parts = rel.split("/");
|
|
715
|
+
try {
|
|
716
|
+
await lstat(join(ws, ...parts));
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
catch (err) {
|
|
720
|
+
if (err.code === "ENOTDIR")
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
for (let i = 1; i < parts.length; i++) {
|
|
724
|
+
try {
|
|
725
|
+
if (!(await lstat(join(ws, ...parts.slice(0, i)))).isDirectory())
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
catch {
|
|
729
|
+
return false; // that folder doesn't exist, so nothing below it does either
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
async function isDir(path) {
|
|
735
|
+
try {
|
|
736
|
+
return (await stat(path)).isDirectory();
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/** `git check-ignore`: exit 0 is ignored, 1 is not, anything else isn't a repo. */
|
|
743
|
+
async function runGitCheckIgnore(workspace, file, env) {
|
|
744
|
+
try {
|
|
745
|
+
await promisify(execFile)("git", ["-C", workspace, "check-ignore", "-q", file], { env, timeout: 10_000 });
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
catch (err) {
|
|
749
|
+
return err.code === 1 ? false : undefined;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
//# sourceMappingURL=kiro.js.map
|