@kisev/agentomatic 11.0.0-dev.46.gfbe1e4c6992e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +95 -0
  2. package/README.ru.md +98 -0
  3. package/dist/agent-profiles.d.ts +125 -0
  4. package/dist/agent-profiles.js +820 -0
  5. package/dist/assets/agents/architect.md +67 -0
  6. package/dist/assets/agents/critic.md +55 -0
  7. package/dist/assets/agents/manager.md +40 -0
  8. package/dist/assets/agents/mapper.md +33 -0
  9. package/dist/assets/agents/review.md +37 -0
  10. package/dist/assets/agents/worker.md +78 -0
  11. package/dist/assets/commands/agents-md.md +10 -0
  12. package/dist/assets/commands/askme.md +10 -0
  13. package/dist/assets/commands/ast-grep.md +10 -0
  14. package/dist/assets/commands/briefing.md +10 -0
  15. package/dist/assets/commands/code-explain.md +10 -0
  16. package/dist/assets/commands/code-review.md +10 -0
  17. package/dist/assets/commands/commit-msg.md +10 -0
  18. package/dist/assets/commands/docs-prepare.md +10 -0
  19. package/dist/assets/commands/docs-review.md +10 -0
  20. package/dist/assets/commands/goal.md +10 -0
  21. package/dist/assets/commands/humanize.md +10 -0
  22. package/dist/assets/commands/mattermost-triage.md +10 -0
  23. package/dist/assets/commands/mattermost.md +10 -0
  24. package/dist/assets/commands/mr-prepare.md +10 -0
  25. package/dist/assets/commands/release-prepare.md +10 -0
  26. package/dist/assets/commands/release-review.md +10 -0
  27. package/dist/assets/commands/rtk-stats.md +13 -0
  28. package/dist/assets/commands/rtk.md +10 -0
  29. package/dist/assets/commands/skill-improve.md +10 -0
  30. package/dist/assets/commands/slides-prompts-prepare.md +10 -0
  31. package/dist/assets/commands/spec-manage.md +13 -0
  32. package/dist/assets/commands/stopit.md +10 -0
  33. package/dist/assets/commands/task-prepare.md +10 -0
  34. package/dist/assets/commands/task-review.md +10 -0
  35. package/dist/assets/commands/task-triage.md +10 -0
  36. package/dist/assets/commands/taskmatic.md +10 -0
  37. package/dist/assets/commands/team-retro.md +10 -0
  38. package/dist/assets/commands/team-roadmap.md +10 -0
  39. package/dist/assets/commands/team-sprint-close.md +10 -0
  40. package/dist/assets/commands/team-sprint-start.md +10 -0
  41. package/dist/assets/lsp-catalog.json +30 -0
  42. package/dist/assets/migration-inventory.json +301 -0
  43. package/dist/assets/plugins/memomatic.js +3 -0
  44. package/dist/assets/plugins/rtk.js +3 -0
  45. package/dist/assets/plugins/rules-injector.js +3 -0
  46. package/dist/assets/plugins/zed-bell.js +3 -0
  47. package/dist/catalog.d.ts +8 -0
  48. package/dist/catalog.js +39 -0
  49. package/dist/cli-output.d.ts +27 -0
  50. package/dist/cli-output.js +296 -0
  51. package/dist/cli.d.ts +2 -0
  52. package/dist/cli.js +1135 -0
  53. package/dist/config-setup.d.ts +71 -0
  54. package/dist/config-setup.js +438 -0
  55. package/dist/contracts.d.ts +52 -0
  56. package/dist/contracts.js +102 -0
  57. package/dist/doctor.d.ts +44 -0
  58. package/dist/doctor.js +638 -0
  59. package/dist/generate-assets.d.ts +1 -0
  60. package/dist/generate-assets.js +54 -0
  61. package/dist/index.d.ts +125 -0
  62. package/dist/index.js +141 -0
  63. package/dist/installer.d.ts +56 -0
  64. package/dist/installer.js +710 -0
  65. package/dist/jsonc.d.ts +56 -0
  66. package/dist/jsonc.js +432 -0
  67. package/dist/lifecycle.d.ts +77 -0
  68. package/dist/lifecycle.js +736 -0
  69. package/dist/package-metadata.d.ts +4 -0
  70. package/dist/package-metadata.js +42 -0
  71. package/dist/plugins/memomatic.d.ts +76 -0
  72. package/dist/plugins/memomatic.js +101 -0
  73. package/dist/plugins/rtk.d.ts +41 -0
  74. package/dist/plugins/rtk.js +177 -0
  75. package/dist/plugins/rules-injector.d.ts +22 -0
  76. package/dist/plugins/rules-injector.js +131 -0
  77. package/dist/plugins/zed-bell.d.ts +13 -0
  78. package/dist/plugins/zed-bell.js +14 -0
  79. package/dist/reconcile.d.ts +47 -0
  80. package/dist/reconcile.js +490 -0
  81. package/dist/registry.d.ts +21 -0
  82. package/dist/registry.js +92 -0
  83. package/dist/routing.d.ts +133 -0
  84. package/dist/routing.js +578 -0
  85. package/dist/runtime/state.d.ts +9 -0
  86. package/dist/runtime/state.js +193 -0
  87. package/dist/runtime/worktree.d.ts +33 -0
  88. package/dist/runtime/worktree.js +220 -0
  89. package/dist/terminal-wizard.d.ts +3 -0
  90. package/dist/terminal-wizard.js +237 -0
  91. package/package.json +72 -0
@@ -0,0 +1,193 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, open, readFile, readdir, rm } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { appendPrivate, withLifecycleLock, writeAtomic } from "../lifecycle.js";
6
+ function home(value = process.env.HOME ?? homedir()) {
7
+ if (!isAbsolute(value) || value.split(sep).includes(".."))
8
+ throw new Error("HOME must be an absolute safe path");
9
+ return resolve(value);
10
+ }
11
+ function inside(root, target) {
12
+ const value = relative(root, target);
13
+ return value === "" || (!value.startsWith(`..${sep}`) && value !== "..");
14
+ }
15
+ export function stateRoot(name, homePath) {
16
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
17
+ throw new Error("unsafe state name");
18
+ const configured = process.env.XDG_STATE_HOME;
19
+ if (configured && (!isAbsolute(configured) || configured.split(sep).includes("..")))
20
+ throw new Error("XDG_STATE_HOME must be an absolute safe path");
21
+ const base = configured ? resolve(configured) : join(home(homePath), ".local", "state");
22
+ return join(base, "opencode", "skills", name);
23
+ }
24
+ export function configRoot(name, homePath) {
25
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
26
+ throw new Error("unsafe config name");
27
+ const base = process.env.XDG_CONFIG_HOME
28
+ ? resolve(process.env.XDG_CONFIG_HOME)
29
+ : join(home(homePath), ".config");
30
+ return join(base, "opencode", "skill-config", name);
31
+ }
32
+ async function safeDirectory(path, boundary, create = false) {
33
+ const target = resolve(path);
34
+ if (!inside(boundary, target))
35
+ throw new Error("state path escapes its boundary");
36
+ try {
37
+ const info = await lstat(boundary);
38
+ if (!info.isDirectory() || info.isSymbolicLink())
39
+ throw new Error("state directory is unsafe");
40
+ }
41
+ catch (error) {
42
+ if (error.code !== "ENOENT")
43
+ throw error;
44
+ }
45
+ if (create) {
46
+ let current = resolve(target).startsWith(sep) ? sep : "";
47
+ for (const piece of resolve(target).split(sep).filter(Boolean)) {
48
+ current = join(current, piece);
49
+ try {
50
+ const info = await lstat(current);
51
+ if (!info.isDirectory() || info.isSymbolicLink())
52
+ throw new Error("state directory is unsafe");
53
+ }
54
+ catch (error) {
55
+ if (error.code !== "ENOENT")
56
+ throw error;
57
+ await mkdir(current, { mode: 0o700 });
58
+ }
59
+ }
60
+ }
61
+ const rootInfo = await lstat(boundary);
62
+ if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink())
63
+ throw new Error("state directory is unsafe");
64
+ let current = resolve(boundary);
65
+ for (const piece of relative(current, target).split(sep).filter(Boolean)) {
66
+ current = join(current, piece);
67
+ const info = await lstat(current);
68
+ if (!info.isDirectory() || info.isSymbolicLink())
69
+ throw new Error("state directory is unsafe");
70
+ }
71
+ }
72
+ export async function readState(path, boundary) {
73
+ if (!inside(boundary, path))
74
+ throw new Error("state path escapes its boundary");
75
+ try {
76
+ await safeDirectory(dirname(path), boundary);
77
+ const info = await lstat(path);
78
+ if (!info.isFile() || info.isSymbolicLink())
79
+ throw new Error("state file is unsafe");
80
+ return JSON.parse(await readFile(path, "utf8"));
81
+ }
82
+ catch (error) {
83
+ if (error.code === "ENOENT")
84
+ return undefined;
85
+ throw error;
86
+ }
87
+ }
88
+ export async function listState(boundary, suffix = ".json") {
89
+ try {
90
+ await safeDirectory(boundary, boundary);
91
+ return (await readdir(boundary)).filter((name) => name.endsWith(suffix)).sort();
92
+ }
93
+ catch (error) {
94
+ if (error.code === "ENOENT")
95
+ return [];
96
+ throw error;
97
+ }
98
+ }
99
+ export async function writeState(path, boundary, value) {
100
+ if (!inside(boundary, path))
101
+ throw new Error("state path escapes its boundary");
102
+ await withLifecycleLock(boundary, async () => {
103
+ await safeDirectory(dirname(path), boundary, true);
104
+ await writeAtomic(path, Buffer.from(`${JSON.stringify(value)}\n`), 0o600);
105
+ });
106
+ }
107
+ function stableValue(value) {
108
+ if (Array.isArray(value))
109
+ return value.map(stableValue);
110
+ if (value !== null && typeof value === "object")
111
+ return Object.fromEntries(Object.entries(value)
112
+ .sort(([left], [right]) => left.localeCompare(right))
113
+ .map(([key, item]) => [key, stableValue(item)]));
114
+ return value;
115
+ }
116
+ async function immutableState(path, boundary, content) {
117
+ await safeDirectory(dirname(path), boundary, true);
118
+ let created = false;
119
+ try {
120
+ const file = await open(path, "wx", 0o600);
121
+ created = true;
122
+ try {
123
+ await file.writeFile(content);
124
+ await file.sync();
125
+ }
126
+ finally {
127
+ await file.close();
128
+ }
129
+ const directory = await open(dirname(path), "r");
130
+ try {
131
+ await directory.sync();
132
+ }
133
+ finally {
134
+ await directory.close();
135
+ }
136
+ }
137
+ catch (error) {
138
+ if (created) {
139
+ try {
140
+ await rm(path, { force: true });
141
+ const directory = await open(dirname(path), "r");
142
+ try {
143
+ await directory.sync();
144
+ }
145
+ finally {
146
+ await directory.close();
147
+ }
148
+ }
149
+ catch {
150
+ // Preserve the write failure; a later immutable-content check remains fail-closed.
151
+ }
152
+ throw error;
153
+ }
154
+ if (error.code !== "EEXIST")
155
+ throw error;
156
+ const info = await lstat(path);
157
+ if (!info.isFile() || info.isSymbolicLink() || !(await readFile(path)).equals(content))
158
+ throw new Error("immutable state history was changed");
159
+ }
160
+ }
161
+ export async function writeVersionedState(path, boundary, logicalName, value) {
162
+ if (!inside(boundary, path))
163
+ throw new Error("state path escapes its boundary");
164
+ const body = Buffer.from(`${JSON.stringify(stableValue(value))}\n`);
165
+ const history = join(boundary, "history", digest({ logicalName }));
166
+ try {
167
+ const info = await lstat(path);
168
+ if (!info.isFile() || info.isSymbolicLink())
169
+ throw new Error("state file is unsafe");
170
+ const previous = await readFile(path);
171
+ await immutableState(join(history, `${createHash("sha256").update(previous).digest("hex")}.json`), boundary, previous);
172
+ }
173
+ catch (error) {
174
+ if (error.code !== "ENOENT")
175
+ throw error;
176
+ }
177
+ await safeDirectory(dirname(path), boundary, true);
178
+ await writeAtomic(path, body, 0o600);
179
+ }
180
+ export async function appendState(path, boundary, value) {
181
+ if (!inside(boundary, path))
182
+ throw new Error("state path escapes its boundary");
183
+ await withLifecycleLock(boundary, async () => {
184
+ await safeDirectory(dirname(path), boundary, true);
185
+ await appendPrivate(path, value);
186
+ });
187
+ }
188
+ export async function withStateLock(boundary, callback) {
189
+ return withLifecycleLock(boundary, callback);
190
+ }
191
+ export function digest(value) {
192
+ return createHash("sha256").update(JSON.stringify(value), "utf8").digest("hex");
193
+ }
@@ -0,0 +1,33 @@
1
+ export type WorktreeStatus = "managed" | "missing" | "blocked" | "unknown";
2
+ export type WorktreeRecord = {
3
+ schema_version: 1;
4
+ workspace_id: string;
5
+ project: string;
6
+ path: string;
7
+ branch: string;
8
+ start_ref: string;
9
+ repository_fingerprint: string;
10
+ marker: string;
11
+ status: WorktreeStatus;
12
+ created_at: string;
13
+ updated_at: string;
14
+ };
15
+ export declare function worktreePlan(project: string, workspaceID?: `${string}-${string}-${string}-${string}-${string}`): Promise<Record<string, unknown>>;
16
+ export declare function worktreeCreate(input: {
17
+ project: string;
18
+ workspace_id: string;
19
+ path?: string;
20
+ branch?: string;
21
+ start_ref?: string;
22
+ }): Promise<WorktreeRecord>;
23
+ export declare function worktreeStatus(project: string, workspaceID: string): Promise<WorktreeRecord | undefined>;
24
+ export declare function worktreeList(project: string): Promise<WorktreeRecord[]>;
25
+ export declare function worktreeRelease(project: string, workspaceID: string): Promise<{
26
+ status: "released" | "blocked";
27
+ workspace_id: string;
28
+ reason?: string;
29
+ }>;
30
+ export declare function worktreeRecover(project: string): Promise<{
31
+ recovered: number;
32
+ blocked: number;
33
+ }>;
@@ -0,0 +1,220 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { chmod, lstat, mkdir, readFile, readdir } from "node:fs/promises";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { digest, listState, readState, stateRoot, withStateLock, writeVersionedState, } from "./state.js";
7
+ import { appendPrivate, writeAtomic } from "../lifecycle.js";
8
+ const run = promisify(execFile);
9
+ const SCHEMA_VERSION = 1;
10
+ function markerPath(path) {
11
+ return join(path, ".opencode-worktree.json");
12
+ }
13
+ function root(project) {
14
+ return join(stateRoot("worktree"), digest(resolve(project)));
15
+ }
16
+ function recordPath(project, id) {
17
+ return join(root(project), "workspaces", `${id}.json`);
18
+ }
19
+ async function git(project, args) {
20
+ const result = await run("git", args, { cwd: project, maxBuffer: 1024 * 1024 });
21
+ return result.stdout.trim();
22
+ }
23
+ async function fingerprint(project) {
24
+ const common = await git(project, ["rev-parse", "--git-common-dir"]);
25
+ return digest({ common: resolve(project, common) });
26
+ }
27
+ async function privateDirectory(path) {
28
+ const info = await lstat(path);
29
+ if (!info.isDirectory() || info.isSymbolicLink() || (info.mode & 0o077) !== 0)
30
+ throw new Error("workspace directory is unsafe");
31
+ }
32
+ async function marker(path) {
33
+ try {
34
+ const info = await lstat(markerPath(path));
35
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || (info.mode & 0o077) !== 0)
36
+ throw new Error("workspace marker is unsafe");
37
+ return JSON.parse(await readFile(markerPath(path), "utf8"));
38
+ }
39
+ catch (error) {
40
+ if (error.code === "ENOENT")
41
+ return undefined;
42
+ throw error;
43
+ }
44
+ }
45
+ async function saveRecord(item, event) {
46
+ const state = root(item.project);
47
+ item.updated_at = new Date().toISOString();
48
+ await writeVersionedState(recordPath(item.project, item.workspace_id), state, `workspace:${item.workspace_id}`, item);
49
+ await appendPrivate(join(state, "receipts.jsonl"), {
50
+ schema_version: 1,
51
+ event,
52
+ workspace_id: item.workspace_id,
53
+ status: item.status,
54
+ at: item.updated_at,
55
+ });
56
+ await appendPrivate(join(state, event.startsWith("release") ? "release.journal.jsonl" : "create.journal.jsonl"), {
57
+ schema_version: 1,
58
+ event,
59
+ workspace_id: item.workspace_id,
60
+ path: item.path,
61
+ at: item.updated_at,
62
+ });
63
+ }
64
+ async function records(project) {
65
+ const state = root(project);
66
+ const directory = join(state, "workspaces");
67
+ const values = await Promise.all((await listState(directory)).map((name) => readState(recordPath(project, name.slice(0, -5)), state)));
68
+ for (const value of values)
69
+ if (value && value.schema_version !== SCHEMA_VERSION)
70
+ throw new Error("unsupported_schema");
71
+ return values.filter((v) => Boolean(v));
72
+ }
73
+ async function inspect(item) {
74
+ try {
75
+ await privateDirectory(item.path);
76
+ const actual = await marker(item.path);
77
+ if (!actual ||
78
+ actual.workspace_id !== item.workspace_id ||
79
+ actual.repository_fingerprint !== item.repository_fingerprint ||
80
+ actual.project !== item.project ||
81
+ actual.marker !== item.marker)
82
+ return "unknown";
83
+ const registered = await git(item.project, ["worktree", "list", "--porcelain"]);
84
+ if (!registered.split("\n").some((line) => line === `worktree ${item.path}`))
85
+ return "unknown";
86
+ if ((await fingerprint(item.path)) !== item.repository_fingerprint)
87
+ return "blocked";
88
+ const status = await git(item.path, ["status", "--porcelain"]);
89
+ const foreignChanges = status
90
+ .split("\n")
91
+ .filter((line) => line && line !== "?? .opencode-worktree.json");
92
+ return foreignChanges.length ? "blocked" : "managed";
93
+ }
94
+ catch (error) {
95
+ if (error.code === "ENOENT")
96
+ return "missing";
97
+ return "blocked";
98
+ }
99
+ }
100
+ export async function worktreePlan(project, workspaceID = randomUUID()) {
101
+ const rootProject = resolve(project);
102
+ const repo = await fingerprint(rootProject);
103
+ return {
104
+ schema_version: SCHEMA_VERSION,
105
+ workspace_id: workspaceID,
106
+ project: rootProject,
107
+ path: join(stateRoot("worktree"), "managed", digest(rootProject), workspaceID),
108
+ start_ref: await git(rootProject, ["rev-parse", "HEAD"]),
109
+ repository_fingerprint: repo,
110
+ };
111
+ }
112
+ export async function worktreeCreate(input) {
113
+ const project = resolve(input.project);
114
+ const state = root(project);
115
+ return withStateLock(state, async () => {
116
+ const existing = await readState(recordPath(project, input.workspace_id), state);
117
+ if (existing) {
118
+ const status = await inspect(existing);
119
+ existing.status = status;
120
+ await saveRecord(existing, "create.idempotent");
121
+ if (status !== "managed")
122
+ throw new Error(`workspace ${status}`);
123
+ return existing;
124
+ }
125
+ const repo = await fingerprint(project);
126
+ const path = resolve(input.path ?? join(stateRoot("worktree"), "managed", digest(project), input.workspace_id));
127
+ const branch = input.branch ?? `opencode/${input.workspace_id}`;
128
+ const start = input.start_ref ?? (await git(project, ["rev-parse", "HEAD"]));
129
+ try {
130
+ const existingPath = await lstat(path);
131
+ if (!existingPath.isDirectory() || existingPath.isSymbolicLink())
132
+ throw new Error("workspace path is unsafe");
133
+ throw new Error((await readdir(path)).length
134
+ ? "workspace path is unknown and not empty"
135
+ : "workspace path is unknown");
136
+ }
137
+ catch (error) {
138
+ if (error.code !== "ENOENT")
139
+ throw error;
140
+ }
141
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
142
+ await git(project, ["worktree", "add", "-b", branch, path, start]);
143
+ await chmod(path, 0o700);
144
+ const markerValue = {
145
+ schema_version: 1,
146
+ workspace_id: input.workspace_id,
147
+ project,
148
+ repository_fingerprint: repo,
149
+ marker: randomUUID(),
150
+ };
151
+ await writeAtomic(markerPath(path), Buffer.from(`${JSON.stringify(markerValue)}\n`), 0o600);
152
+ const item = {
153
+ schema_version: 1,
154
+ workspace_id: input.workspace_id,
155
+ project,
156
+ path,
157
+ branch,
158
+ start_ref: start,
159
+ repository_fingerprint: repo,
160
+ marker: String(markerValue.marker),
161
+ status: "managed",
162
+ created_at: new Date().toISOString(),
163
+ updated_at: new Date().toISOString(),
164
+ };
165
+ await saveRecord(item, "create.completed");
166
+ return item;
167
+ });
168
+ }
169
+ export async function worktreeStatus(project, workspaceID) {
170
+ const item = await readState(recordPath(resolve(project), workspaceID), root(resolve(project)));
171
+ if (!item)
172
+ return undefined;
173
+ if (item.schema_version !== SCHEMA_VERSION)
174
+ throw new Error("unsupported_schema");
175
+ item.status = await inspect(item);
176
+ return item;
177
+ }
178
+ export async function worktreeList(project) {
179
+ const result = await records(resolve(project));
180
+ for (const item of result)
181
+ item.status = await inspect(item);
182
+ return result;
183
+ }
184
+ export async function worktreeRelease(project, workspaceID) {
185
+ const projectRoot = resolve(project);
186
+ return withStateLock(root(projectRoot), async () => {
187
+ const item = await worktreeStatus(projectRoot, workspaceID);
188
+ if (!item)
189
+ throw new Error("workspace not found");
190
+ if (item.status !== "managed")
191
+ return { status: "blocked", workspace_id: workspaceID, reason: item.status };
192
+ try {
193
+ await git(item.project, ["worktree", "remove", "--force", item.path]);
194
+ item.status = "missing";
195
+ await saveRecord(item, "release.completed");
196
+ return { status: "released", workspace_id: workspaceID };
197
+ }
198
+ catch (error) {
199
+ return { status: "blocked", workspace_id: workspaceID, reason: String(error) };
200
+ }
201
+ });
202
+ }
203
+ export async function worktreeRecover(project) {
204
+ const projectRoot = resolve(project);
205
+ const items = await worktreeList(projectRoot);
206
+ const result = { recovered: 0, blocked: 0 };
207
+ await withStateLock(root(projectRoot), async () => {
208
+ for (const item of items) {
209
+ if (item.status === "managed") {
210
+ result.recovered += 1;
211
+ await saveRecord(item, "recover.completed");
212
+ }
213
+ else if (item.status !== "missing") {
214
+ result.blocked += 1;
215
+ await saveRecord(item, "recover.blocked");
216
+ }
217
+ }
218
+ });
219
+ return result;
220
+ }
@@ -0,0 +1,3 @@
1
+ export declare function selectOption(label: string, options: readonly string[], stdin?: NodeJS.ReadStream, stderr?: NodeJS.WriteStream, initial?: number): Promise<number | null>;
2
+ export declare function selectOptions(label: string, options: readonly string[], initialSelected?: readonly string[], stdin?: NodeJS.ReadStream, stderr?: NodeJS.WriteStream): Promise<string[] | null>;
3
+ export declare function promptText(label: string, stdin?: NodeJS.ReadStream, stderr?: NodeJS.WriteStream): Promise<string | null>;
@@ -0,0 +1,237 @@
1
+ import { InstallerError } from "./installer.js";
2
+ function parseKey(buffer) {
3
+ const seq = buffer.toString();
4
+ if (seq === "\r" || seq === "\n")
5
+ return { name: "return", sequence: seq };
6
+ if (seq === " ")
7
+ return { name: "space", sequence: seq };
8
+ if (seq === "a" || seq === "A")
9
+ return { name: "all", sequence: seq };
10
+ if (seq === "n" || seq === "N")
11
+ return { name: "none", sequence: seq };
12
+ if (seq === "\x03")
13
+ return { name: "ctrl+c", sequence: seq };
14
+ if (seq === "\x04")
15
+ return { name: "ctrl+d", sequence: seq };
16
+ if (seq === "\x1b" || seq === "\x1b\x1b" || seq === "\x1b\x1b\x1b")
17
+ return { name: "escape", sequence: seq };
18
+ if (seq === "\x1b[A" || seq === "\x1bOA" || seq === "\x1b[OA")
19
+ return { name: "up", sequence: seq };
20
+ if (seq === "\x1b[B" || seq === "\x1bOB" || seq === "\x1b[OB")
21
+ return { name: "down", sequence: seq };
22
+ if (seq === "\x1b[C" || seq === "\x1bOC" || seq === "\x1b[OC")
23
+ return { name: "right", sequence: seq };
24
+ if (seq === "\x1b[D" || seq === "\x1bOD" || seq === "\x1b[OD")
25
+ return { name: "left", sequence: seq };
26
+ return { name: "unknown", sequence: seq };
27
+ }
28
+ function nextSequence(remaining) {
29
+ if (remaining.startsWith("\x1b[O"))
30
+ return remaining.slice(0, 4);
31
+ if (remaining.startsWith("\x1b[") || remaining.startsWith("\x1bO"))
32
+ return remaining.slice(0, 3);
33
+ return remaining[0];
34
+ }
35
+ function validateOptions(options) {
36
+ if (options.length === 0)
37
+ throw new InstallerError("invalid_input", "Interactive selector requires at least one option");
38
+ if (new Set(options).size !== options.length)
39
+ throw new InstallerError("invalid_input", "Interactive selector options must be unique");
40
+ }
41
+ function clearLines(stderr, count) {
42
+ for (let i = 0; i < count; i++) {
43
+ stderr.write("\x1b[A\x1b[K");
44
+ }
45
+ }
46
+ export async function selectOption(label, options, stdin = process.stdin, stderr = process.stderr, initial = 0) {
47
+ if (!stdin.isTTY || !stderr.isTTY) {
48
+ throw new InstallerError("terminal_required", "This operation requires an interactive terminal");
49
+ }
50
+ validateOptions(options);
51
+ let selected = Math.max(0, Math.min(initial, options.length - 1));
52
+ const lineCount = options.length + 2;
53
+ function render() {
54
+ stderr.write(`? ${label}\n`);
55
+ stderr.write(" Up/Down move | Enter select | Esc cancel\n");
56
+ for (let i = 0; i < options.length; i++) {
57
+ const cursor = i === selected ? "> " : " ";
58
+ stderr.write(`${cursor}${options[i]}\n`);
59
+ }
60
+ }
61
+ render();
62
+ return new Promise((resolve) => {
63
+ stdin.setRawMode(true);
64
+ stdin.resume();
65
+ function cleanup() {
66
+ stdin.setRawMode(false);
67
+ stdin.pause();
68
+ stdin.removeListener("data", onData);
69
+ }
70
+ function onData(data) {
71
+ let remaining = data.toString();
72
+ while (remaining) {
73
+ const sequence = nextSequence(remaining);
74
+ remaining = remaining.slice(sequence.length);
75
+ const key = parseKey(Buffer.from(sequence));
76
+ if (key.name === "ctrl+c" || key.name === "ctrl+d" || key.name === "escape") {
77
+ cleanup();
78
+ clearLines(stderr, lineCount);
79
+ resolve(null);
80
+ return;
81
+ }
82
+ if (key.name === "return") {
83
+ cleanup();
84
+ clearLines(stderr, lineCount);
85
+ resolve(selected);
86
+ return;
87
+ }
88
+ if (key.name === "up") {
89
+ clearLines(stderr, lineCount);
90
+ selected = (selected - 1 + options.length) % options.length;
91
+ render();
92
+ }
93
+ else if (key.name === "down") {
94
+ clearLines(stderr, lineCount);
95
+ selected = (selected + 1) % options.length;
96
+ render();
97
+ }
98
+ }
99
+ }
100
+ stdin.on("data", onData);
101
+ });
102
+ }
103
+ export async function selectOptions(label, options, initialSelected = [], stdin = process.stdin, stderr = process.stderr) {
104
+ if (!stdin.isTTY || !stderr.isTTY) {
105
+ throw new InstallerError("terminal_required", "This operation requires an interactive terminal");
106
+ }
107
+ validateOptions(options);
108
+ const allowed = new Set(options);
109
+ for (const value of initialSelected) {
110
+ if (!allowed.has(value))
111
+ throw new InstallerError("invalid_input", `Unknown initial selector option: ${value}`);
112
+ }
113
+ const selected = new Set(initialSelected);
114
+ let active = 0;
115
+ const lineCount = options.length + 3;
116
+ function render() {
117
+ stderr.write(`? ${label}\n`);
118
+ stderr.write(" Up/Down move | Space toggle | A all | N none | Enter confirm | Esc cancel\n");
119
+ for (let index = 0; index < options.length; index += 1) {
120
+ const cursor = index === active ? "> " : " ";
121
+ const marker = selected.has(options[index]) ? "[x]" : "[ ]";
122
+ stderr.write(`${cursor}${marker} ${options[index]}\n`);
123
+ }
124
+ stderr.write(` Selected ${selected.size}/${options.length}\n`);
125
+ }
126
+ render();
127
+ return new Promise((resolve) => {
128
+ stdin.setRawMode(true);
129
+ stdin.resume();
130
+ function cleanup() {
131
+ stdin.setRawMode(false);
132
+ stdin.pause();
133
+ stdin.removeListener("data", onData);
134
+ }
135
+ function finish(value) {
136
+ cleanup();
137
+ clearLines(stderr, lineCount);
138
+ resolve(value);
139
+ }
140
+ function redraw(update) {
141
+ clearLines(stderr, lineCount);
142
+ update();
143
+ render();
144
+ }
145
+ function onData(data) {
146
+ let remaining = data.toString();
147
+ while (remaining) {
148
+ const sequence = nextSequence(remaining);
149
+ remaining = remaining.slice(sequence.length);
150
+ const key = parseKey(Buffer.from(sequence));
151
+ if (key.name === "ctrl+c" || key.name === "ctrl+d" || key.name === "escape") {
152
+ finish(null);
153
+ return;
154
+ }
155
+ if (key.name === "return") {
156
+ finish(options.filter((option) => selected.has(option)));
157
+ return;
158
+ }
159
+ if (key.name === "up") {
160
+ redraw(() => {
161
+ active = (active - 1 + options.length) % options.length;
162
+ });
163
+ }
164
+ else if (key.name === "down") {
165
+ redraw(() => {
166
+ active = (active + 1) % options.length;
167
+ });
168
+ }
169
+ else if (key.name === "space") {
170
+ redraw(() => {
171
+ const option = options[active];
172
+ if (selected.has(option))
173
+ selected.delete(option);
174
+ else
175
+ selected.add(option);
176
+ });
177
+ }
178
+ else if (key.name === "all") {
179
+ redraw(() => {
180
+ for (const option of options)
181
+ selected.add(option);
182
+ });
183
+ }
184
+ else if (key.name === "none") {
185
+ redraw(() => selected.clear());
186
+ }
187
+ }
188
+ }
189
+ stdin.on("data", onData);
190
+ });
191
+ }
192
+ export async function promptText(label, stdin = process.stdin, stderr = process.stderr) {
193
+ if (!stdin.isTTY || !stderr.isTTY) {
194
+ throw new InstallerError("terminal_required", "This operation requires an interactive terminal");
195
+ }
196
+ stderr.write(`? ${label}\n`);
197
+ stderr.write(" Type a value | Enter confirm | Esc cancel\n");
198
+ stderr.write("> ");
199
+ return new Promise((resolve) => {
200
+ let buffer = "";
201
+ stdin.setRawMode(true);
202
+ stdin.resume();
203
+ function cleanup() {
204
+ stdin.setRawMode(false);
205
+ stdin.pause();
206
+ stdin.removeListener("data", onData);
207
+ }
208
+ function onData(data) {
209
+ for (const seq of data.toString()) {
210
+ if (seq === "\r" || seq === "\n") {
211
+ cleanup();
212
+ stderr.write("\n");
213
+ resolve(buffer.trim() || null);
214
+ return;
215
+ }
216
+ if (seq === "\x03" || seq === "\x04" || seq === "\x1b") {
217
+ cleanup();
218
+ stderr.write("\n");
219
+ resolve(null);
220
+ return;
221
+ }
222
+ if (seq === "\x7f" || seq === "\b") {
223
+ if (buffer.length > 0) {
224
+ buffer = buffer.slice(0, -1);
225
+ stderr.write("\x1b[D \x1b[D");
226
+ }
227
+ continue;
228
+ }
229
+ if (seq >= " ") {
230
+ buffer += seq;
231
+ stderr.write(seq);
232
+ }
233
+ }
234
+ }
235
+ stdin.on("data", onData);
236
+ });
237
+ }