@zjaaal/artemis 0.0.0-stage → 0.1.2
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 +568 -2
- package/dist/cli/constants.d.ts +30 -0
- package/dist/cli/constants.js +34 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/init-command.d.ts +8 -0
- package/dist/cli/init-command.js +42 -0
- package/dist/cli/init.d.ts +23 -0
- package/dist/cli/init.js +153 -0
- package/dist/cli/launch.d.ts +10 -0
- package/dist/cli/launch.js +116 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +7 -0
- package/dist/cli/resolve-hook.d.ts +9 -0
- package/dist/cli/resolve-hook.js +21 -0
- package/dist/cli/settings.d.ts +18 -0
- package/dist/cli/settings.js +110 -0
- package/dist/constants.d.ts +94 -0
- package/dist/constants.js +86 -0
- package/dist/context/context.d.ts +6 -0
- package/dist/context/context.js +35 -0
- package/dist/define/define.d.ts +15 -0
- package/dist/define/define.js +99 -0
- package/dist/discover/discover.d.ts +7 -0
- package/dist/discover/discover.js +99 -0
- package/dist/edges/files.d.ts +25 -0
- package/dist/edges/files.js +111 -0
- package/dist/edges/modules.d.ts +2 -0
- package/dist/edges/modules.js +6 -0
- package/dist/edges/process.d.ts +2 -0
- package/dist/edges/process.js +9 -0
- package/dist/edges/prompts.d.ts +2 -0
- package/dist/edges/prompts.js +14 -0
- package/dist/edges/system.d.ts +4 -0
- package/dist/edges/system.js +5 -0
- package/dist/errors/errors.d.ts +25 -0
- package/dist/errors/errors.js +37 -0
- package/dist/help/help.d.ts +15 -0
- package/dist/help/help.js +169 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/kits/terminal/constants.d.ts +10 -0
- package/dist/kits/terminal/constants.js +10 -0
- package/dist/kits/terminal/index.d.ts +3 -0
- package/dist/kits/terminal/index.js +2 -0
- package/dist/kits/terminal/models.d.ts +57 -0
- package/dist/kits/terminal/models.js +2 -0
- package/dist/kits/terminal/open-link.d.ts +2 -0
- package/dist/kits/terminal/open-link.js +12 -0
- package/dist/kits/terminal/open-tabs.d.ts +3 -0
- package/dist/kits/terminal/open-tabs.js +5 -0
- package/dist/kits/terminal/toml.d.ts +3 -0
- package/dist/kits/terminal/toml.js +53 -0
- package/dist/kits/terminal/warp.d.ts +6 -0
- package/dist/kits/terminal/warp.js +32 -0
- package/dist/kits/worktrees/constants.d.ts +25 -0
- package/dist/kits/worktrees/constants.js +25 -0
- package/dist/kits/worktrees/git.d.ts +2 -0
- package/dist/kits/worktrees/git.js +7 -0
- package/dist/kits/worktrees/index.d.ts +3 -0
- package/dist/kits/worktrees/index.js +2 -0
- package/dist/kits/worktrees/instances.d.ts +9 -0
- package/dist/kits/worktrees/instances.js +33 -0
- package/dist/kits/worktrees/lock-edges.d.ts +7 -0
- package/dist/kits/worktrees/lock-edges.js +21 -0
- package/dist/kits/worktrees/lock.d.ts +7 -0
- package/dist/kits/worktrees/lock.js +104 -0
- package/dist/kits/worktrees/models.d.ts +123 -0
- package/dist/kits/worktrees/models.js +2 -0
- package/dist/kits/worktrees/names.d.ts +5 -0
- package/dist/kits/worktrees/names.js +9 -0
- package/dist/kits/worktrees/queries.d.ts +20 -0
- package/dist/kits/worktrees/queries.js +61 -0
- package/dist/kits/worktrees/registration.d.ts +15 -0
- package/dist/kits/worktrees/registration.js +107 -0
- package/dist/kits/worktrees/registry-store.d.ts +3 -0
- package/dist/kits/worktrees/registry-store.js +27 -0
- package/dist/kits/worktrees/worktrees.d.ts +10 -0
- package/dist/kits/worktrees/worktrees.js +56 -0
- package/dist/models.d.ts +485 -0
- package/dist/models.js +1 -0
- package/dist/parse/parse.d.ts +8 -0
- package/dist/parse/parse.js +81 -0
- package/dist/resolve/resolve.d.ts +18 -0
- package/dist/resolve/resolve.js +96 -0
- package/dist/run/report.d.ts +6 -0
- package/dist/run/report.js +51 -0
- package/dist/run/run.d.ts +8 -0
- package/dist/run/run.js +163 -0
- package/dist/run/world.d.ts +3 -0
- package/dist/run/world.js +12 -0
- package/dist/utils/error-code.d.ts +3 -0
- package/dist/utils/error-code.js +7 -0
- package/dist/utils/error-message.d.ts +3 -0
- package/dist/utils/error-message.js +2 -0
- package/dist/utils/is-record.d.ts +3 -0
- package/dist/utils/is-record.js +2 -0
- package/dist/utils/tag.d.ts +3 -0
- package/dist/utils/tag.js +8 -0
- package/dist/utils/terminator.d.ts +5 -0
- package/dist/utils/terminator.js +8 -0
- package/package.json +72 -4
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ALIAS_SEPARATOR, FLAG_PREFIX, HELP_WORD, ID_SEPARATOR, GLOBAL_FLAG_TOKENS, SUGGEST_LENGTH_DIVISOR, SUGGEST_MIN_DISTANCE, TERMINATOR } from '../constants.js';
|
|
2
|
+
/** One row of the edit-distance table, from the row above it. */
|
|
3
|
+
function nextRow(previous, char, target, rowNumber) {
|
|
4
|
+
const row = [rowNumber];
|
|
5
|
+
target.split('').forEach((targetChar, index) => {
|
|
6
|
+
const substitution = (previous[index] ?? 0) + (char === targetChar ? 0 : 1);
|
|
7
|
+
const deletion = (previous[index + 1] ?? 0) + 1;
|
|
8
|
+
const insertion = (row[index] ?? 0) + 1;
|
|
9
|
+
row.push(Math.min(substitution, deletion, insertion));
|
|
10
|
+
});
|
|
11
|
+
return row;
|
|
12
|
+
}
|
|
13
|
+
function editDistance(a, b) {
|
|
14
|
+
const firstRow = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
15
|
+
const lastRow = a
|
|
16
|
+
.split('')
|
|
17
|
+
.reduce((previous, char, index) => nextRow(previous, char, b, index + 1), firstRow);
|
|
18
|
+
return lastRow[b.length] ?? 0;
|
|
19
|
+
}
|
|
20
|
+
const maxSuggestDistance = (name) => Math.max(SUGGEST_MIN_DISTANCE, Math.floor(name.length / SUGGEST_LENGTH_DIVISOR));
|
|
21
|
+
/** The closest command, topic or alias name, if one is close enough to be a likely typo. */
|
|
22
|
+
export const suggest = (registry, name) => {
|
|
23
|
+
const names = [
|
|
24
|
+
...registry.commands.keys(),
|
|
25
|
+
...registry.topics.keys(),
|
|
26
|
+
...registry.aliases.keys()
|
|
27
|
+
];
|
|
28
|
+
const [best] = names
|
|
29
|
+
.map((candidate) => ({ candidate, score: editDistance(name, candidate) }))
|
|
30
|
+
.sort((a, b) => a.score - b.score);
|
|
31
|
+
return best && best.score <= maxSuggestDistance(name) ? best.candidate : undefined;
|
|
32
|
+
};
|
|
33
|
+
/** A command by id or by alias. */
|
|
34
|
+
export const findCommand = (registry, id) => {
|
|
35
|
+
const aliased = registry.aliases.get(id);
|
|
36
|
+
return registry.commands.get(id) ?? (aliased ? registry.commands.get(aliased) : undefined);
|
|
37
|
+
};
|
|
38
|
+
const isFlagToken = (token) => token.startsWith(FLAG_PREFIX);
|
|
39
|
+
/** The words that can name a command: everything before the first flag. */
|
|
40
|
+
function leadingWords(tokens) {
|
|
41
|
+
const firstFlag = tokens.findIndex(isFlagToken);
|
|
42
|
+
return firstFlag === -1 ? tokens : tokens.slice(0, firstFlag);
|
|
43
|
+
}
|
|
44
|
+
// "uve" and "uve help" both show the topic; anything else after a topic is unknown
|
|
45
|
+
const isHelpAfterTopic = (words, length) => length === words.length - 1 && words[length] === HELP_WORD;
|
|
46
|
+
const isTopicOnly = (words, length) => length === words.length || isHelpAfterTopic(words, length);
|
|
47
|
+
function resolveTopic(topic, words, length, tokens) {
|
|
48
|
+
return isTopicOnly(words, length)
|
|
49
|
+
? { kind: 'topic', topic, rest: tokens.slice(length) }
|
|
50
|
+
: { kind: 'unknown', name: words.slice(0, length + 1).join(ID_SEPARATOR) };
|
|
51
|
+
}
|
|
52
|
+
function matchLongest(registry, tokens, words) {
|
|
53
|
+
for (let length = words.length; length > 0; length--) {
|
|
54
|
+
const id = words.slice(0, length).join(ID_SEPARATOR);
|
|
55
|
+
const command = registry.commands.get(id);
|
|
56
|
+
const topic = registry.topics.get(id);
|
|
57
|
+
if (command) {
|
|
58
|
+
return { kind: 'command', entry: command, rest: tokens.slice(length) };
|
|
59
|
+
}
|
|
60
|
+
if (topic) {
|
|
61
|
+
return resolveTopic(topic, words, length, tokens);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Works out which command (or topic) the command line names. Words are matched greedily, so
|
|
68
|
+
* `uve config set http://x` is the command `uve config set` with `http://x` left as its argument.
|
|
69
|
+
* `uve:config:set` and declared aliases are accepted as well, and a topic followed by `help` (`uve help`)
|
|
70
|
+
* shows that topic.
|
|
71
|
+
*/
|
|
72
|
+
export const resolve = (registry, argv) => {
|
|
73
|
+
const [first, ...others] = argv;
|
|
74
|
+
if (first === undefined) {
|
|
75
|
+
return { kind: 'unknown', name: '' };
|
|
76
|
+
}
|
|
77
|
+
const aliased = registry.aliases.has(first) ? findCommand(registry, first) : undefined;
|
|
78
|
+
if (aliased) {
|
|
79
|
+
return { kind: 'command', entry: aliased, rest: others };
|
|
80
|
+
}
|
|
81
|
+
const tokens = first.includes(ALIAS_SEPARATOR)
|
|
82
|
+
? [...first.split(ALIAS_SEPARATOR), ...others]
|
|
83
|
+
: [...argv];
|
|
84
|
+
const words = leadingWords(tokens);
|
|
85
|
+
return matchLongest(registry, tokens, words) ?? { kind: 'unknown', name: words[0] ?? first };
|
|
86
|
+
};
|
|
87
|
+
export const isGlobalFlagToken = (token) => GLOBAL_FLAG_TOKENS.includes(token);
|
|
88
|
+
/**
|
|
89
|
+
* Splits off the flags typed before the command name (`art --json math add 1 2`). They belong to
|
|
90
|
+
* the run, not to a command; `--` ends them like it ends any flags.
|
|
91
|
+
*/
|
|
92
|
+
export const splitLeadingFlags = (argv) => {
|
|
93
|
+
const firstWord = argv.findIndex((token) => !isFlagToken(token) || token === TERMINATOR);
|
|
94
|
+
const end = firstWord === -1 ? argv.length : firstWord;
|
|
95
|
+
return { leading: argv.slice(0, end), rest: argv.slice(end) };
|
|
96
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { EXIT_CODE, FAILURE_CODES } from '../constants.js';
|
|
2
|
+
import { isCommandError, isUsageError } from '../errors/errors.js';
|
|
3
|
+
import { messageOf } from '../utils/error-message.js';
|
|
4
|
+
/** The exit code of a tagged error, trusted only when it is a failure code: a failed run never exits 0. */
|
|
5
|
+
const failureCodeOf = (value) => FAILURE_CODES.find((code) => code === value) ?? EXIT_CODE.FAILED;
|
|
6
|
+
/** Splits an error into what the user sees; only a plain error (a bug) carries its stack. */
|
|
7
|
+
function describeError(error) {
|
|
8
|
+
const isExpected = isCommandError(error);
|
|
9
|
+
return {
|
|
10
|
+
message: messageOf(error),
|
|
11
|
+
hint: isExpected ? error.hint : undefined,
|
|
12
|
+
usage: isUsageError(error) ? error.usage : undefined,
|
|
13
|
+
stack: !isExpected && error instanceof Error ? error.stack : undefined,
|
|
14
|
+
exitCode: isExpected ? failureCodeOf(error.exitCode) : EXIT_CODE.FAILED
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function toJson(report, commandId) {
|
|
18
|
+
return JSON.stringify({
|
|
19
|
+
ok: false,
|
|
20
|
+
command: commandId ?? null,
|
|
21
|
+
error: report.message,
|
|
22
|
+
...(report.hint ? { hint: report.hint } : {})
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function toText(report, { style }) {
|
|
26
|
+
const lines = [`${style.red('Error:')} ${report.message}`];
|
|
27
|
+
if (report.hint) {
|
|
28
|
+
lines.push(`${style.yellow('Hint:')} ${report.hint}`);
|
|
29
|
+
}
|
|
30
|
+
if (report.usage) {
|
|
31
|
+
lines.push('', `${style.bold('Usage')} ${report.usage}`);
|
|
32
|
+
}
|
|
33
|
+
if (report.stack) {
|
|
34
|
+
lines.push('', style.dim(report.stack));
|
|
35
|
+
}
|
|
36
|
+
return lines.join('\n');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Prints an error the way the run was asked to: a JSON object on stdout under --json, text on
|
|
40
|
+
* stderr otherwise. Returns the exit code.
|
|
41
|
+
*/
|
|
42
|
+
export const reportError = (error, context) => {
|
|
43
|
+
const report = describeError(error);
|
|
44
|
+
if (context.json) {
|
|
45
|
+
context.io.stdout.write(`${toJson(report, context.commandId)}\n`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
context.io.stderr.write(`${toText(report, context)}\n`);
|
|
49
|
+
}
|
|
50
|
+
return report.exitCode;
|
|
51
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RunCliFn } from '../models.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runs a command line and resolves to the exit code; it never calls process.exit, so it can be
|
|
4
|
+
* embedded and tested. Exit codes: 0 success, 1 the command failed, 2 the command line was wrong.
|
|
5
|
+
* With --json every call prints one object on stdout: `{ ok: true, command, result }` or
|
|
6
|
+
* `{ ok: false, command, error, hint }`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const run: RunCliFn;
|
package/dist/run/run.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createContext } from '../context/context.js';
|
|
2
|
+
import { EXIT_CODE, FLAG_PREFIX, HELP_WORD, ID_SEPARATOR } from '../constants.js';
|
|
3
|
+
import { discover } from '../discover/discover.js';
|
|
4
|
+
import { CommandError, UsageError, isUsageError } from '../errors/errors.js';
|
|
5
|
+
import { commandHelp, manifest, rootHelp, topicHelp, topicManifest, usage } from '../help/help.js';
|
|
6
|
+
import { parse } from '../parse/parse.js';
|
|
7
|
+
import { reportError } from './report.js';
|
|
8
|
+
import { resolveWorld } from './world.js';
|
|
9
|
+
import { findCommand, isGlobalFlagToken, resolve, splitLeadingFlags, suggest } from '../resolve/resolve.js';
|
|
10
|
+
import { jsonRequested } from '../utils/terminator.js';
|
|
11
|
+
const isFlagToken = (token) => token.startsWith(FLAG_PREFIX);
|
|
12
|
+
/** Prints text, or under --json the data wrapped in the success envelope. */
|
|
13
|
+
function print(session, text, data) {
|
|
14
|
+
const output = session.wantsJson
|
|
15
|
+
? JSON.stringify({ ok: true, command: session.state.commandId ?? null, result: data })
|
|
16
|
+
: text;
|
|
17
|
+
session.io.stdout.write(`${output}\n`);
|
|
18
|
+
}
|
|
19
|
+
const unknownHint = (bin, registry, name) => {
|
|
20
|
+
const match = suggest(registry, name);
|
|
21
|
+
if (match) {
|
|
22
|
+
return `Did you mean "${match}"?`;
|
|
23
|
+
}
|
|
24
|
+
// Inside a topic, point at that topic's list rather than the whole CLI
|
|
25
|
+
const topic = name.split(ID_SEPARATOR).slice(0, -1).join(ID_SEPARATOR);
|
|
26
|
+
const listing = registry.topics.has(topic) ? `${bin} ${topic} help` : `${bin} help`;
|
|
27
|
+
return `Run "${listing}" to see the commands.`;
|
|
28
|
+
};
|
|
29
|
+
const unknownCommand = ({ bin, registry }, name) => new UsageError(`Unknown command "${name}".`, { hint: unknownHint(bin, registry, name) });
|
|
30
|
+
/** A bad call to `ctx.invoke` is the calling command's bug, not a wrong command line: exit 1. */
|
|
31
|
+
function parseNested(session, target, id, argv) {
|
|
32
|
+
try {
|
|
33
|
+
const nested = parse(target.definition, argv, usage(session.bin, target));
|
|
34
|
+
if (nested.global.help) {
|
|
35
|
+
throw new CommandError(`ctx.invoke("${id}"): --help is not something a command can invoke`, {
|
|
36
|
+
hint: `Call it with its arguments, or read its help with "${session.bin} help ${id}".`
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return nested;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (!isUsageError(error)) {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
throw new CommandError(`ctx.invoke("${id}"): ${error.message}`, {
|
|
46
|
+
hint: error.usage ? `Usage: ${error.usage}` : error.hint,
|
|
47
|
+
cause: error
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** `ctx.invoke`: runs another command, which inherits --json and --yes from the caller. */
|
|
52
|
+
const createInvoke = (session, caller) => async (id, argv = []) => {
|
|
53
|
+
const target = findCommand(session.registry, id);
|
|
54
|
+
if (!target) {
|
|
55
|
+
throw new CommandError(`ctx.invoke: there is no command "${id}"`);
|
|
56
|
+
}
|
|
57
|
+
const nested = parseNested(session, target, id, argv);
|
|
58
|
+
const global = {
|
|
59
|
+
...nested.global,
|
|
60
|
+
json: nested.global.json || caller.json,
|
|
61
|
+
yes: nested.global.yes || caller.yes
|
|
62
|
+
};
|
|
63
|
+
return execute(session, target, { ...nested, global });
|
|
64
|
+
};
|
|
65
|
+
async function execute(session, entry, parsed) {
|
|
66
|
+
const ctx = createContext({
|
|
67
|
+
io: session.io,
|
|
68
|
+
global: parsed.global,
|
|
69
|
+
// --json promises exactly one object on stdout, so nothing may prompt there either
|
|
70
|
+
interactive: session.canPrompt && !parsed.global.yes && !parsed.global.json,
|
|
71
|
+
style: session.style,
|
|
72
|
+
registry: session.registry,
|
|
73
|
+
invoke: createInvoke(session, parsed.global)
|
|
74
|
+
});
|
|
75
|
+
return entry.definition.run({ args: parsed.args, flags: parsed.flags, ctx });
|
|
76
|
+
}
|
|
77
|
+
/** The text and the --json data for something that was resolved. */
|
|
78
|
+
function helpFor(session, target) {
|
|
79
|
+
const { bin, registry, style } = session;
|
|
80
|
+
const everything = manifest(bin, registry);
|
|
81
|
+
if (target.kind === 'topic') {
|
|
82
|
+
return {
|
|
83
|
+
text: topicHelp(bin, registry, target.topic, style),
|
|
84
|
+
data: topicManifest(everything, target.topic.id)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
text: commandHelp(bin, target.entry, style),
|
|
89
|
+
data: everything.commands.find((command) => command.id === target.entry.id)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function showHelpFor(session, words) {
|
|
93
|
+
if (!words.length) {
|
|
94
|
+
session.state.commandId = HELP_WORD;
|
|
95
|
+
print(session, rootHelp(session.bin, session.registry, session.style), manifest(session.bin, session.registry));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const target = resolve(session.registry, words);
|
|
99
|
+
if (target.kind === 'unknown') {
|
|
100
|
+
throw unknownCommand(session, target.name);
|
|
101
|
+
}
|
|
102
|
+
session.state.commandId = target.kind === 'command' ? target.entry.id : target.topic.id;
|
|
103
|
+
const { text, data } = helpFor(session, target);
|
|
104
|
+
print(session, text, data);
|
|
105
|
+
}
|
|
106
|
+
/** Works out what the command line asks for and does it. */
|
|
107
|
+
async function dispatch(session, argv) {
|
|
108
|
+
const { leading, rest } = splitLeadingFlags(argv);
|
|
109
|
+
const stray = leading.find((token) => !isGlobalFlagToken(token));
|
|
110
|
+
if (stray) {
|
|
111
|
+
throw new UsageError(`Unknown option "${stray}"`, {
|
|
112
|
+
hint: `Flags go after the command: ${session.bin} <command> ${stray}`
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const [first] = rest;
|
|
116
|
+
if (first === undefined || first === HELP_WORD) {
|
|
117
|
+
showHelpFor(session, rest.slice(1).filter((token) => !isFlagToken(token)));
|
|
118
|
+
return EXIT_CODE.OK;
|
|
119
|
+
}
|
|
120
|
+
const target = resolve(session.registry, rest);
|
|
121
|
+
if (target.kind === 'unknown') {
|
|
122
|
+
throw unknownCommand(session, target.name);
|
|
123
|
+
}
|
|
124
|
+
if (target.kind === 'topic') {
|
|
125
|
+
showHelpFor(session, target.topic.id.split(ID_SEPARATOR));
|
|
126
|
+
return EXIT_CODE.OK;
|
|
127
|
+
}
|
|
128
|
+
session.state.commandId = target.entry.id;
|
|
129
|
+
const parsed = parse(target.entry.definition, [...leading, ...target.rest], usage(session.bin, target.entry));
|
|
130
|
+
if (parsed.global.help) {
|
|
131
|
+
showHelpFor(session, target.entry.id.split(ID_SEPARATOR));
|
|
132
|
+
return EXIT_CODE.OK;
|
|
133
|
+
}
|
|
134
|
+
const result = await execute(session, target.entry, parsed);
|
|
135
|
+
if (session.wantsJson) {
|
|
136
|
+
print(session, '', result ?? null);
|
|
137
|
+
}
|
|
138
|
+
return EXIT_CODE.OK;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Runs a command line and resolves to the exit code; it never calls process.exit, so it can be
|
|
142
|
+
* embedded and tested. Exit codes: 0 success, 1 the command failed, 2 the command line was wrong.
|
|
143
|
+
* With --json every call prints one object on stdout: `{ ok: true, command, result }` or
|
|
144
|
+
* `{ ok: false, command, error, hint }`.
|
|
145
|
+
*/
|
|
146
|
+
export const run = async (argv, options) => {
|
|
147
|
+
const world = { ...resolveWorld(options), bin: options.bin };
|
|
148
|
+
const wantsJson = jsonRequested(argv);
|
|
149
|
+
const state = { commandId: undefined };
|
|
150
|
+
try {
|
|
151
|
+
const registry = await discover(options.commandsDir);
|
|
152
|
+
registry.warnings.forEach((warning) => world.io.stderr.write(`${world.style.yellow('warning:')} ${warning}\n`));
|
|
153
|
+
return await dispatch({ ...world, registry, wantsJson, state }, argv);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
return reportError(error, {
|
|
157
|
+
io: world.io,
|
|
158
|
+
style: world.style,
|
|
159
|
+
json: wantsJson,
|
|
160
|
+
commandId: state.commandId
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DEFAULT_COLOR_ENV } from '../constants.js';
|
|
2
|
+
import { currentProcess } from '../edges/process.js';
|
|
3
|
+
import { createStyle } from '../help/help.js';
|
|
4
|
+
/** Works out where output goes, whether to color and whether prompts may show. */
|
|
5
|
+
export const resolveWorld = (options) => {
|
|
6
|
+
const system = currentProcess();
|
|
7
|
+
const io = { stdout: options.stdout ?? system.stdout, stderr: options.stderr ?? system.stderr };
|
|
8
|
+
const env = options.env ?? system.env;
|
|
9
|
+
const color = options.color ?? (Boolean(io.stdout.isTTY) && !env[DEFAULT_COLOR_ENV]);
|
|
10
|
+
const canPrompt = options.interactive ?? (system.stdinIsTTY && system.stdoutIsTTY);
|
|
11
|
+
return { io, style: createStyle(color), canPrompt };
|
|
12
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The `code` of a Node system error ("ENOENT", "EEXIST", ...), if the error has one. */
|
|
2
|
+
export const codeOf = (error) => {
|
|
3
|
+
if (!(error instanceof Error) || !('code' in error)) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
return typeof error.code === 'string' ? error.code : undefined;
|
|
7
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Whether a value carries a `Symbol.for` tag, which survives two copies of the package being loaded. */
|
|
2
|
+
export const hasTag = (value, tag) => {
|
|
3
|
+
if (typeof value !== 'object' || value === null) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const found = Reflect.get(value, tag);
|
|
7
|
+
return Boolean(found);
|
|
8
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BeforeTerminatorFn, JsonRequestedFn } from '../models.js';
|
|
2
|
+
/** The words before `--`: after it everything is a plain argument, never a flag. */
|
|
3
|
+
export declare const beforeTerminator: BeforeTerminatorFn;
|
|
4
|
+
/** Whether the command line asks for `--json`. After `--` it is a plain argument, so it does not count. */
|
|
5
|
+
export declare const jsonRequested: JsonRequestedFn;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { JSON_FLAG, TERMINATOR } from '../constants.js';
|
|
2
|
+
/** The words before `--`: after it everything is a plain argument, never a flag. */
|
|
3
|
+
export const beforeTerminator = (argv) => {
|
|
4
|
+
const end = argv.indexOf(TERMINATOR);
|
|
5
|
+
return argv.slice(0, end === -1 ? argv.length : end);
|
|
6
|
+
};
|
|
7
|
+
/** Whether the command line asks for `--json`. After `--` it is a plain argument, so it does not count. */
|
|
8
|
+
export const jsonRequested = (argv) => beforeTerminator(argv).includes(JSON_FLAG);
|
package/package.json
CHANGED
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zjaaal/artemis",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Drop one file in a folder and get a CLI command. A command framework agents can write and read.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@10.34.7",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"art": "dist/cli/main.js",
|
|
12
|
+
"artemis": "dist/cli/main.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./worktrees": {
|
|
26
|
+
"types": "./dist/kits/worktrees/index.d.ts",
|
|
27
|
+
"import": "./dist/kits/worktrees/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./terminal": {
|
|
30
|
+
"types": "./dist/kits/terminal/index.d.ts",
|
|
31
|
+
"import": "./dist/kits/terminal/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./cli": {
|
|
34
|
+
"types": "./dist/cli/index.d.ts",
|
|
35
|
+
"import": "./dist/cli/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/zJaaal/artemis.git"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -p tsconfig.build.json",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:all": "tsc --noEmit && eslint . && prettier --check . && vitest run",
|
|
47
|
+
"check-exports": "node scripts/check-exports.mjs",
|
|
48
|
+
"check-lock": "node scripts/check-lock.mjs",
|
|
49
|
+
"check-bin": "node scripts/check-bin.mjs"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"prompts": "2.4.2"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@types/prompts": "^2.4.9"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"@types/prompts": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "22.20.5",
|
|
64
|
+
"@types/prompts": "2.4.9",
|
|
65
|
+
"@vitest/eslint-plugin": "1.6.27",
|
|
66
|
+
"eslint": "9.39.5",
|
|
67
|
+
"eslint-config-prettier": "10.1.8",
|
|
68
|
+
"eslint-plugin-prettier": "5.5.7",
|
|
69
|
+
"prettier": "3.9.10",
|
|
70
|
+
"typescript": "6.0.3",
|
|
71
|
+
"typescript-eslint": "8.72.0",
|
|
72
|
+
"vitest": "4.1.12"
|
|
73
|
+
}
|
|
74
|
+
}
|