@krapht/ork 0.3.0 → 1.0.0-alpha.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/README.md +16 -12
- package/dist/__build_info__-DdTVd2p5.mjs +615 -0
- package/dist/bin.mjs +1572 -909
- package/dist/clean-NnboOuz6.mjs +88 -0
- package/dist/completion-snnYGjgK.mjs +405 -0
- package/dist/{context-BFGC_C02.mjs → context-DO3sBy9v.mjs} +30 -191
- package/dist/decide-B81wCZQg.mjs +113 -0
- package/dist/{find-BcvhT6UN.mjs → find-BKW-uGey.mjs} +1 -1
- package/dist/{format-DqpObyMl.mjs → format-CmTlTOjT.mjs} +72 -198
- package/dist/help-groups-BwYoAWsh.mjs +354 -0
- package/dist/{in-a-run-BbLhZOFW.mjs → in-a-run-BPXX09Bs.mjs} +1 -1
- package/dist/{limits-BKYAmqCR.mjs → limits-JCve2QpI.mjs} +1 -1
- package/dist/list-C7UEgmtd.mjs +110 -0
- package/dist/{mcp-BWsdQyri.mjs → mcp-CKPyNaKD.mjs} +7 -7
- package/dist/{output-Q2EdeVUt.mjs → output-B1V6ZJSx.mjs} +130 -36
- package/dist/{pause-DKACDfV1.mjs → pause-B_SL_yDz.mjs} +4 -4
- package/dist/{relay-CbK-Wme9.mjs → relay-o-Luee1c.mjs} +2 -2
- package/dist/remove-DAZ7ZCzt.mjs +194 -0
- package/dist/{send-C6yr04Fe.mjs → send-3B64fBNB.mjs} +3 -3
- package/dist/{src-D5ruZFgD.mjs → src-BJTegEzY.mjs} +1212 -57
- package/dist/{src-DtN1k-B3.mjs → src-C38BPzit.mjs} +251 -18
- package/dist/{src-Cz1yOUTg.mjs → src-DHvWU4tX.mjs} +2742 -1659
- package/dist/{src-CZOVF2yU.mjs → src-Dyz_Gy1q.mjs} +35 -8
- package/dist/ui/THIRD_PARTY_LICENSES.txt +2 -2
- package/dist/ui/assets/app-C8nJ6NpT.js +67 -0
- package/dist/ui/assets/app-CH7YC3in.css +1 -0
- package/dist/ui/assets/{index-BiO54L90.css → index-BPIbqRRb.css} +1 -1
- package/dist/ui/assets/{index-BoG6O-Lp.js → index-D83F2OKZ.js} +2 -2
- package/dist/ui/index.html +2 -3
- package/dist/ui-CY4xxJUu.mjs +472 -0
- package/dist/{usage-BIjf9Xdt.mjs → usage-CJnytoe4.mjs} +6 -5
- package/dist/{watch-B8o4Szd_.mjs → watch-BJB82ZJ7.mjs} +7 -7
- package/dist/workflows-CiM7S3l0.mjs +88 -0
- package/dist/worktrees-86ySXxR6.mjs +212 -0
- package/package.json +6 -6
- package/dist/__build_info__-CXXMLvjt.mjs +0 -104
- package/dist/ui/assets/app-BlDdP396.js +0 -61
- package/dist/ui/assets/app-C722xNZ3.css +0 -1
- package/dist/ui-CZS1XKla.mjs +0 -160
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { A as gitOk, O as removeWorktree } from "./src-DHvWU4tX.mjs";
|
|
2
|
+
// ../core/src/worktree/clean.ts
|
|
3
|
+
/**
|
|
4
|
+
* @file What `ork worktrees clean` and `ork worktrees remove` do with what
|
|
5
|
+
* list.ts reads: the plan (what goes, what's kept and why), by one rule for
|
|
6
|
+
* every caller, and carrying it out. The CLI and the Worktrees panel both
|
|
7
|
+
* plan here, so neither removes what the other would keep.
|
|
8
|
+
*/
|
|
9
|
+
/** What names an item for `remove`: its branch, or its worktree's path. */
|
|
10
|
+
const keyOf = (item) => item.branch ?? item.path ?? `${item.runId}/${item.nodeId}`;
|
|
11
|
+
/** The reasons no `force` overrides: removing would break something. */
|
|
12
|
+
const MAIN_CHECKOUT = "checked out in the main checkout";
|
|
13
|
+
const RUN_GOING = "its run is still going";
|
|
14
|
+
const kept = (item, skip) => ({
|
|
15
|
+
item,
|
|
16
|
+
removeWorktree: false,
|
|
17
|
+
deleteBranch: false,
|
|
18
|
+
skip
|
|
19
|
+
});
|
|
20
|
+
/** What clean does with one item. */
|
|
21
|
+
const cleanStep = (item, options) => {
|
|
22
|
+
if (item.inMainCheckout) return kept(item, MAIN_CHECKOUT);
|
|
23
|
+
if (item.heldBy !== void 0) return kept(item, `checked out in ${item.heldBy}`);
|
|
24
|
+
if (options.live?.has(item.runId) === true) return kept(item, RUN_GOING);
|
|
25
|
+
if (item.dirty && !options.force) return kept(item, "uncommitted changes");
|
|
26
|
+
if (item.branch === void 0 && item.checkedOut === void 0 && item.ahead > 0 && !options.force) return kept(item, "commits on no branch");
|
|
27
|
+
const removable = item.path !== null && !item.missing;
|
|
28
|
+
if (options.branches && item.branch !== void 0 && !item.merged && !options.force) return {
|
|
29
|
+
item,
|
|
30
|
+
removeWorktree: removable,
|
|
31
|
+
deleteBranch: false,
|
|
32
|
+
skip: "branch not merged"
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
item,
|
|
36
|
+
removeWorktree: removable,
|
|
37
|
+
deleteBranch: options.branches && item.branch !== void 0
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/** Whether a step drops a worktree whose folder is gone, by its own path. */
|
|
41
|
+
const stepPrunes = (step) => step.item.missing && step.item.path !== null && step.skip === void 0;
|
|
42
|
+
/** Whether a step does anything. */
|
|
43
|
+
const stepActs = (step) => step.removeWorktree || step.deleteBranch || stepPrunes(step);
|
|
44
|
+
/**
|
|
45
|
+
* What `ork worktrees clean` would do. A worktree with uncommitted changes, an
|
|
46
|
+
* unmerged branch, or a detached worktree holding commits no branch has, is
|
|
47
|
+
* kept unless `force`; one checked out in the main checkout or in a worktree
|
|
48
|
+
* Ork didn't create, or whose run is still going, is kept even then.
|
|
49
|
+
*/
|
|
50
|
+
const planClean = (items, filter, options) => items.filter((item) => filter.runId === void 0 || item.runId === filter.runId).filter((item) => filter.key === void 0 || keyOf(item) === filter.key).filter((item) => !filter.merged || item.merged).map((item) => cleanStep(item, options));
|
|
51
|
+
/**
|
|
52
|
+
* Carries out one step; what it did goes into `done` as it goes. It touches
|
|
53
|
+
* only its own item: a missing worktree is dropped by its own path (`git
|
|
54
|
+
* worktree remove`), never by `git worktree prune`, which would drop every
|
|
55
|
+
* missing worktree in the repository, the user's own included.
|
|
56
|
+
*/
|
|
57
|
+
const applyStep = async (root, step, force, done) => {
|
|
58
|
+
const { item } = step;
|
|
59
|
+
if (stepPrunes(step) && !step.removeWorktree && item.path !== null) {
|
|
60
|
+
await gitOk(root, [
|
|
61
|
+
"worktree",
|
|
62
|
+
"remove",
|
|
63
|
+
item.path
|
|
64
|
+
]);
|
|
65
|
+
done.push(`pruned ${item.path}`);
|
|
66
|
+
}
|
|
67
|
+
if (step.removeWorktree && item.path !== null) {
|
|
68
|
+
await removeWorktree(root, item.path, force);
|
|
69
|
+
done.push(`removed ${item.path}`);
|
|
70
|
+
}
|
|
71
|
+
if (step.deleteBranch && item.branch !== void 0) {
|
|
72
|
+
await gitOk(root, [
|
|
73
|
+
"branch",
|
|
74
|
+
force ? "-D" : "-d",
|
|
75
|
+
item.branch
|
|
76
|
+
]);
|
|
77
|
+
done.push(`deleted ${item.branch}`);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Carries out a clean plan, step by step. What it did goes into `done` as it
|
|
82
|
+
* goes, so a caller still has it when git fails halfway.
|
|
83
|
+
*/
|
|
84
|
+
const applyClean = async (root, steps, force, done = []) => {
|
|
85
|
+
for (const step of steps) await applyStep(root, step, force, done);
|
|
86
|
+
return done;
|
|
87
|
+
};
|
|
88
|
+
export { planClean as a, keyOf as i, applyStep as n, stepActs as o, cleanStep as r, stepPrunes as s, applyClean as t };
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { Qn as NodeIdSchema, ar as CLAUDE_DRIVERS, bn as placeFiles, ht as liveStatus, xn as placeFor } from "./src-DHvWU4tX.mjs";
|
|
2
|
+
import { a as builtinWorkflowNames, n as pendingDecisions, o as builtinWorkflowText, s as isBuiltinArg } from "./decide-B81wCZQg.mjs";
|
|
3
|
+
import { t as findRun } from "./find-BKW-uGey.mjs";
|
|
4
|
+
import { c as openStore, l as storePath, o as writeErr } from "./output-B1V6ZJSx.mjs";
|
|
5
|
+
import { a as findEntry, c as subcommandsOf, d as SHELL_SCRIPTS, l as COMPLETE_COMMAND, n as groupedEntries, r as argInfoOf, t as groupOf, u as SHELLS } from "./help-groups-BwYoAWsh.mjs";
|
|
6
|
+
import { a as resolveWorkflowArg, n as WORKFLOW_SUFFIX } from "./workflows-CiM7S3l0.mjs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { parse } from "yaml";
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
10
|
+
import { defineCommand } from "citty";
|
|
11
|
+
/** The store at `~/.ork/state.db`, read-only; none when it can't be read. */
|
|
12
|
+
const readOnlyStore = (file = storePath()) => {
|
|
13
|
+
if (!existsSync(file)) return void 0;
|
|
14
|
+
try {
|
|
15
|
+
return openStore(file, { readOnly: true });
|
|
16
|
+
} catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/** The project `cwd` is in, from the store's registered projects if any. */
|
|
21
|
+
const placeOf = (cwd, store) => placeFor(cwd, store?.projects() ?? []);
|
|
22
|
+
/** The project's workflows by name, then Ork's own by their `ork:` name. */
|
|
23
|
+
const workflowCandidates = (place) => {
|
|
24
|
+
const folder = placeFiles(place).workflows;
|
|
25
|
+
return [...(existsSync(folder) ? readdirSync(folder).filter((name) => name.endsWith(WORKFLOW_SUFFIX)).map((name) => name.slice(0, -WORKFLOW_SUFFIX.length)).toSorted() : []).map((value) => ({
|
|
26
|
+
value,
|
|
27
|
+
description: "this project's workflow"
|
|
28
|
+
})), ...builtinWorkflowNames().map((value) => ({
|
|
29
|
+
value,
|
|
30
|
+
description: "one of Ork's workflows"
|
|
31
|
+
}))];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A workflow argument's text: Ork's own (never written out here), or the file
|
|
35
|
+
* the argument names as every command resolves it; none for a folder.
|
|
36
|
+
*/
|
|
37
|
+
const workflowText = (arg, cwd, place) => {
|
|
38
|
+
if (isBuiltinArg(arg)) return builtinWorkflowText(arg);
|
|
39
|
+
const file = path.resolve(cwd, resolveWorkflowArg(arg, cwd, placeFiles(place).workflows));
|
|
40
|
+
return existsSync(file) && statSync(file).isFile() ? readFileSync(file, "utf8") : void 0;
|
|
41
|
+
};
|
|
42
|
+
/** The node ids of the workflow `arg` names; none when it can't be read. */
|
|
43
|
+
const workflowNodes = (arg, cwd, place) => {
|
|
44
|
+
const text = workflowText(arg, cwd, place);
|
|
45
|
+
const parsed = text === void 0 ? void 0 : parse(text);
|
|
46
|
+
const nodes = parsed !== null && typeof parsed === "object" && "nodes" in parsed ? parsed.nodes : void 0;
|
|
47
|
+
return (nodes !== null && typeof nodes === "object" ? Object.keys(nodes) : []).filter((key) => NodeIdSchema.safeParse(key).success).map((value) => ({
|
|
48
|
+
value,
|
|
49
|
+
description: arg
|
|
50
|
+
}));
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Runs whose id starts with `prefix`, newest first: the project's, else any
|
|
54
|
+
* project's, at most `RUN_LIMIT`.
|
|
55
|
+
*/
|
|
56
|
+
const runCandidates = (store, place, prefix) => {
|
|
57
|
+
const starts = (id) => id.startsWith(prefix);
|
|
58
|
+
const own = store.runs({
|
|
59
|
+
project: place.root,
|
|
60
|
+
limit: 50
|
|
61
|
+
}).filter((run) => starts(run.id));
|
|
62
|
+
const fallback = prefix === "" ? store.runs({ limit: 50 }) : store.lookup(prefix).slice(0, 50);
|
|
63
|
+
return (own.length > 0 ? own : fallback).map((run) => ({
|
|
64
|
+
value: run.id,
|
|
65
|
+
description: `${run.workflow} · ${liveStatus(run)}`
|
|
66
|
+
}));
|
|
67
|
+
};
|
|
68
|
+
/** The run `arg` names, or the project's newest; its nodes in order. */
|
|
69
|
+
const runNodes = (store, arg, cwd) => {
|
|
70
|
+
const found = findRun(store, arg, cwd);
|
|
71
|
+
if (!found.ok) return [];
|
|
72
|
+
const { id } = found.run;
|
|
73
|
+
return store.nodes(id).map((value) => ({
|
|
74
|
+
value,
|
|
75
|
+
description: `run ${id}`
|
|
76
|
+
}));
|
|
77
|
+
};
|
|
78
|
+
/** Whose decisions: the run `arg` names, else the project's runs. */
|
|
79
|
+
const decisionFilter = (store, arg, cwd, place) => {
|
|
80
|
+
if (arg === void 0) return { project: place.root };
|
|
81
|
+
const found = findRun(store, arg, cwd);
|
|
82
|
+
return found.ok ? { runId: found.run.id } : void 0;
|
|
83
|
+
};
|
|
84
|
+
/** The open decisions of the run `arg` names, else of the project's runs. */
|
|
85
|
+
const decisionCandidates = (store, arg, cwd, place) => {
|
|
86
|
+
const filter = decisionFilter(store, arg, cwd, place);
|
|
87
|
+
return filter === void 0 ? [] : pendingDecisions(store, filter).map((decision) => ({
|
|
88
|
+
value: decision.id,
|
|
89
|
+
description: `${decision.kind} of ${decision.runId}/${decision.node}`
|
|
90
|
+
}));
|
|
91
|
+
};
|
|
92
|
+
// src/completion/values.ts
|
|
93
|
+
const withStore = (context, read) => {
|
|
94
|
+
const store = context.store();
|
|
95
|
+
return store === void 0 ? [] : read(store);
|
|
96
|
+
};
|
|
97
|
+
const runs = {
|
|
98
|
+
group: "runs",
|
|
99
|
+
values: (context) => withStore(context, (store) => runCandidates(store, placeOf(context.cwd, store), context.current))
|
|
100
|
+
};
|
|
101
|
+
const workflows = {
|
|
102
|
+
group: "workflows",
|
|
103
|
+
values: (context) => workflowCandidates(placeOf(context.cwd, context.store()))
|
|
104
|
+
};
|
|
105
|
+
/** A workflow's nodes when the command takes a workflow, else a run's. */
|
|
106
|
+
const nodes = {
|
|
107
|
+
group: "nodes",
|
|
108
|
+
values: (context) => {
|
|
109
|
+
const workflow = context.given["workflow"];
|
|
110
|
+
if (context.positionals.includes("workflow")) return workflow === void 0 ? [] : workflowNodes(workflow, context.cwd, placeOf(context.cwd, context.store()));
|
|
111
|
+
return withStore(context, (store) => runNodes(store, context.given["run"], context.cwd));
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const decisions = {
|
|
115
|
+
group: "decisions",
|
|
116
|
+
values: (context) => withStore(context, (store) => decisionCandidates(store, context.given["run"], context.cwd, placeOf(context.cwd, store)))
|
|
117
|
+
};
|
|
118
|
+
const drivers = {
|
|
119
|
+
group: "drivers",
|
|
120
|
+
values: () => CLAUDE_DRIVERS.map((driver) => ({
|
|
121
|
+
value: driver.id,
|
|
122
|
+
description: driver.label
|
|
123
|
+
}))
|
|
124
|
+
};
|
|
125
|
+
const files = {
|
|
126
|
+
group: "files",
|
|
127
|
+
values: () => [],
|
|
128
|
+
files: true
|
|
129
|
+
};
|
|
130
|
+
const shells = {
|
|
131
|
+
group: "shells",
|
|
132
|
+
values: () => SHELLS.map((value) => ({
|
|
133
|
+
value,
|
|
134
|
+
description: ""
|
|
135
|
+
}))
|
|
136
|
+
};
|
|
137
|
+
/** Providers by argument name, or by command path and name. */
|
|
138
|
+
const VALUE_PROVIDERS = {
|
|
139
|
+
run: runs,
|
|
140
|
+
workflow: workflows,
|
|
141
|
+
files: {
|
|
142
|
+
...workflows,
|
|
143
|
+
files: true
|
|
144
|
+
},
|
|
145
|
+
node: nodes,
|
|
146
|
+
"decide id": decisions,
|
|
147
|
+
"completion shell": shells,
|
|
148
|
+
driver: drivers,
|
|
149
|
+
path: files,
|
|
150
|
+
file: files,
|
|
151
|
+
output: files
|
|
152
|
+
};
|
|
153
|
+
/** The provider for argument `name` of the command at `path`, if any. */
|
|
154
|
+
const providerFor = (providers, path, name) => providers[[...path, name].join(" ")] ?? providers[name];
|
|
155
|
+
// src/completion/candidates.ts
|
|
156
|
+
/**
|
|
157
|
+
* @file What `ork __complete` offers at a position: subcommands (the root's in
|
|
158
|
+
* `ork --help`'s groups), a command's public flags, an argument's values from
|
|
159
|
+
* its provider, or an enum flag's values; filtered by what was typed, and
|
|
160
|
+
* printed one per line as `value<TAB>description`, with `:group <title>`
|
|
161
|
+
* before each group and `:files` when file names fit too.
|
|
162
|
+
*/
|
|
163
|
+
const NONE = {
|
|
164
|
+
sections: [],
|
|
165
|
+
files: false
|
|
166
|
+
};
|
|
167
|
+
/** The first line of a description, as a menu shows it. */
|
|
168
|
+
const brief = (text) => text.split("\n")[0]?.trim() ?? "";
|
|
169
|
+
const subcommandSections = (position) => {
|
|
170
|
+
if (position.path.length > 0) return [{
|
|
171
|
+
title: "commands",
|
|
172
|
+
candidates: position.subcommands.filter((entry) => !entry.hidden).map((entry) => ({
|
|
173
|
+
value: entry.name,
|
|
174
|
+
description: brief(entry.description)
|
|
175
|
+
}))
|
|
176
|
+
}];
|
|
177
|
+
return groupedEntries(position.subcommands).map((group) => ({
|
|
178
|
+
title: group.title,
|
|
179
|
+
candidates: group.entries.map((entry) => ({
|
|
180
|
+
value: entry.name,
|
|
181
|
+
description: groupOf(entry.name)?.short ?? brief(entry.description)
|
|
182
|
+
}))
|
|
183
|
+
}));
|
|
184
|
+
};
|
|
185
|
+
const flagSection = (position) => ({
|
|
186
|
+
title: "flags",
|
|
187
|
+
candidates: [...position.flags.flatMap((flag) => [{
|
|
188
|
+
value: `--${flag.name}`,
|
|
189
|
+
description: brief(flag.description)
|
|
190
|
+
}, ...flag.negatable ? [{
|
|
191
|
+
value: `--no-${flag.name}`,
|
|
192
|
+
description: `Turn off --${flag.name}`
|
|
193
|
+
}] : []]), {
|
|
194
|
+
value: "--help",
|
|
195
|
+
description: "Show this command's help"
|
|
196
|
+
}]
|
|
197
|
+
});
|
|
198
|
+
const fromProvider = (provider, context) => provider === void 0 ? NONE : {
|
|
199
|
+
sections: [{
|
|
200
|
+
title: provider.group,
|
|
201
|
+
candidates: provider.values(context)
|
|
202
|
+
}],
|
|
203
|
+
files: provider.files === true
|
|
204
|
+
};
|
|
205
|
+
/** Everything that fits at `position`, before filtering. */
|
|
206
|
+
const offerAt = (position, providers, context) => {
|
|
207
|
+
const { slot, path } = position;
|
|
208
|
+
if (slot.kind === "flag") return {
|
|
209
|
+
sections: [flagSection(position)],
|
|
210
|
+
files: false
|
|
211
|
+
};
|
|
212
|
+
if (slot.kind === "value") {
|
|
213
|
+
const { flag } = slot;
|
|
214
|
+
if (flag.options.length > 0) {
|
|
215
|
+
const candidates = flag.options.map((value) => ({
|
|
216
|
+
value,
|
|
217
|
+
description: ""
|
|
218
|
+
}));
|
|
219
|
+
return {
|
|
220
|
+
sections: [{
|
|
221
|
+
title: flag.name,
|
|
222
|
+
candidates
|
|
223
|
+
}],
|
|
224
|
+
files: false
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return fromProvider(providerFor(providers, path, flag.name), context);
|
|
228
|
+
}
|
|
229
|
+
const values = slot.positional === void 0 ? NONE : fromProvider(providerFor(providers, path, slot.positional.name), context);
|
|
230
|
+
return {
|
|
231
|
+
sections: [...position.index === 0 ? subcommandSections(position) : [], ...values.sections],
|
|
232
|
+
files: values.files
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
/** What fits at `position`, keeping what starts with the typed word. */
|
|
236
|
+
const offerFor = (position, providers, context) => {
|
|
237
|
+
const offer = offerAt(position, providers, context);
|
|
238
|
+
return {
|
|
239
|
+
sections: offer.sections.map((section) => ({
|
|
240
|
+
...section,
|
|
241
|
+
candidates: section.candidates.filter((c) => c.value.startsWith(position.current))
|
|
242
|
+
})).filter((section) => section.candidates.length > 0),
|
|
243
|
+
files: offer.files
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* A value a shell may insert as it is: no space, control character, quote or
|
|
248
|
+
* shell syntax (`$(…)`, `;`, `*`), and no leading `:`, which would read as one
|
|
249
|
+
* of this output's own lines. Ids, names and flags all fit.
|
|
250
|
+
*/
|
|
251
|
+
const SAFE_VALUE = /^[\w.@+,=/~-][\w.@+,=/~:-]*$/u;
|
|
252
|
+
/** One line per candidate, unsafe values dropped; descriptions on one line. */
|
|
253
|
+
const offerLines = (offer) => [...offer.sections.flatMap((section) => [`:group ${section.title}`, ...section.candidates.filter((c) => SAFE_VALUE.test(c.value)).map((c) => `${c.value}\t${c.description.replaceAll(/\p{Cc}+/gu, " ")}`)]), ...offer.files ? [":files"] : []];
|
|
254
|
+
// src/completion/position.ts
|
|
255
|
+
const flagNamed = (flags, word) => {
|
|
256
|
+
const name = word.replace(/^-{1,2}/u, "").split("=")[0] ?? "";
|
|
257
|
+
return flags.find((flag) => flag.name === name || flag.aliases.includes(name) || flag.negatable && name === `no-${flag.name}`);
|
|
258
|
+
};
|
|
259
|
+
/** One word before the cursor, folded into the walk. */
|
|
260
|
+
const step = async (walk, word) => {
|
|
261
|
+
const { pending, ...settled } = walk;
|
|
262
|
+
if (pending !== void 0) return {
|
|
263
|
+
...settled,
|
|
264
|
+
given: {
|
|
265
|
+
...walk.given,
|
|
266
|
+
[pending.name]: word
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
if (!walk.rest && word === "--") return {
|
|
270
|
+
...walk,
|
|
271
|
+
rest: true
|
|
272
|
+
};
|
|
273
|
+
if (!walk.rest && word.startsWith("-")) {
|
|
274
|
+
const flag = flagNamed(walk.flags, word);
|
|
275
|
+
if (flag === void 0 || !flag.takesValue) return walk;
|
|
276
|
+
if (!word.includes("=")) return {
|
|
277
|
+
...walk,
|
|
278
|
+
pending: flag
|
|
279
|
+
};
|
|
280
|
+
const value = word.slice(word.indexOf("=") + 1);
|
|
281
|
+
return {
|
|
282
|
+
...walk,
|
|
283
|
+
given: {
|
|
284
|
+
...walk.given,
|
|
285
|
+
[flag.name]: value
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const sub = walk.index === 0 ? findEntry(walk.subcommands, word) : void 0;
|
|
290
|
+
if (sub !== void 0) return {
|
|
291
|
+
...await argInfoOf(sub.def),
|
|
292
|
+
path: [...walk.path, sub.name],
|
|
293
|
+
subcommands: await subcommandsOf(sub.def),
|
|
294
|
+
given: walk.given,
|
|
295
|
+
index: 0,
|
|
296
|
+
rest: false
|
|
297
|
+
};
|
|
298
|
+
const positional = walk.positionals[walk.index];
|
|
299
|
+
const given = positional === void 0 ? walk.given : {
|
|
300
|
+
...walk.given,
|
|
301
|
+
[positional.name]: word
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
...walk,
|
|
305
|
+
given,
|
|
306
|
+
index: walk.index + 1
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
const slotOf = (walk, current) => {
|
|
310
|
+
if (walk.pending !== void 0) return {
|
|
311
|
+
kind: "value",
|
|
312
|
+
flag: walk.pending
|
|
313
|
+
};
|
|
314
|
+
if (!walk.rest && current.startsWith("-")) return { kind: "flag" };
|
|
315
|
+
const positional = walk.positionals[walk.index];
|
|
316
|
+
return positional === void 0 ? { kind: "word" } : {
|
|
317
|
+
kind: "word",
|
|
318
|
+
positional
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* Where `words` (what follows `ork`, the word under the cursor last, maybe
|
|
323
|
+
* empty) leave the cursor, in the command `root`.
|
|
324
|
+
*/
|
|
325
|
+
const positionOf = async (root, words) => {
|
|
326
|
+
const before = words.slice(0, -1);
|
|
327
|
+
const current = words.at(-1) ?? "";
|
|
328
|
+
const start = {
|
|
329
|
+
...await argInfoOf(root),
|
|
330
|
+
path: [],
|
|
331
|
+
subcommands: await subcommandsOf(root),
|
|
332
|
+
given: {},
|
|
333
|
+
index: 0,
|
|
334
|
+
rest: false
|
|
335
|
+
};
|
|
336
|
+
const walk = await before.reduce(async (previous, word) => step(await previous, word), Promise.resolve(start));
|
|
337
|
+
const { pending: _pending, rest: _rest, ...reached } = walk;
|
|
338
|
+
return {
|
|
339
|
+
...reached,
|
|
340
|
+
current,
|
|
341
|
+
slot: slotOf(walk, current)
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
// src/commands/completion.ts
|
|
345
|
+
/**
|
|
346
|
+
* The candidates for the last of `words` (what follows `ork`; the word under
|
|
347
|
+
* the cursor, maybe empty, last), as `__complete` prints them.
|
|
348
|
+
*/
|
|
349
|
+
const completionLines = async (root, words, cwd, storeFile) => {
|
|
350
|
+
const position = await positionOf(root, words.length === 0 ? [""] : words);
|
|
351
|
+
const opened = {};
|
|
352
|
+
const store = () => {
|
|
353
|
+
if (!("store" in opened)) opened.store = readOnlyStore(storeFile);
|
|
354
|
+
return opened.store;
|
|
355
|
+
};
|
|
356
|
+
try {
|
|
357
|
+
return offerLines(offerFor(position, VALUE_PROVIDERS, {
|
|
358
|
+
cwd,
|
|
359
|
+
given: position.given,
|
|
360
|
+
current: position.current,
|
|
361
|
+
positionals: position.positionals.map((p) => p.name),
|
|
362
|
+
store
|
|
363
|
+
}));
|
|
364
|
+
} finally {
|
|
365
|
+
opened.store?.close();
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
/** `ork __complete`'s whole job: print the candidates, or nothing. */
|
|
369
|
+
const printCompletions = async (root, args, cwd = process.cwd()) => {
|
|
370
|
+
try {
|
|
371
|
+
const words = args[0] === "--" ? args.slice(1) : args;
|
|
372
|
+
const lines = await completionLines(root, words, cwd);
|
|
373
|
+
process.stdout.write(lines.map((line) => `${line}\n`).join(""));
|
|
374
|
+
} catch {}
|
|
375
|
+
};
|
|
376
|
+
/** The hidden `ork __complete` of the command `root`. */
|
|
377
|
+
const completeCommand = (root) => defineCommand({
|
|
378
|
+
meta: {
|
|
379
|
+
name: COMPLETE_COMMAND,
|
|
380
|
+
description: "Print tab-completion candidates (the scripts run it)",
|
|
381
|
+
hidden: true
|
|
382
|
+
},
|
|
383
|
+
run: ({ rawArgs }) => printCompletions(root, rawArgs)
|
|
384
|
+
});
|
|
385
|
+
const completion = defineCommand({
|
|
386
|
+
meta: {
|
|
387
|
+
name: "completion",
|
|
388
|
+
description: `Print a tab-completion script for your shell: ${SHELLS.join(", ")}`
|
|
389
|
+
},
|
|
390
|
+
args: { shell: {
|
|
391
|
+
type: "positional",
|
|
392
|
+
description: SHELLS.join(", "),
|
|
393
|
+
required: true
|
|
394
|
+
} },
|
|
395
|
+
run: ({ args }) => {
|
|
396
|
+
const script = SHELL_SCRIPTS[args.shell];
|
|
397
|
+
if (script === void 0) {
|
|
398
|
+
writeErr(`No completion for \`${args.shell}\`; there is one for ${SHELLS.join(", ")}.`);
|
|
399
|
+
process.exitCode = 1;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
process.stdout.write(script);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
export { completeCommand, completion, printCompletions };
|