@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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +568 -2
  3. package/dist/cli/constants.d.ts +30 -0
  4. package/dist/cli/constants.js +34 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.js +1 -0
  7. package/dist/cli/init-command.d.ts +8 -0
  8. package/dist/cli/init-command.js +42 -0
  9. package/dist/cli/init.d.ts +23 -0
  10. package/dist/cli/init.js +153 -0
  11. package/dist/cli/launch.d.ts +10 -0
  12. package/dist/cli/launch.js +116 -0
  13. package/dist/cli/main.d.ts +2 -0
  14. package/dist/cli/main.js +7 -0
  15. package/dist/cli/resolve-hook.d.ts +9 -0
  16. package/dist/cli/resolve-hook.js +21 -0
  17. package/dist/cli/settings.d.ts +18 -0
  18. package/dist/cli/settings.js +110 -0
  19. package/dist/constants.d.ts +94 -0
  20. package/dist/constants.js +86 -0
  21. package/dist/context/context.d.ts +6 -0
  22. package/dist/context/context.js +35 -0
  23. package/dist/define/define.d.ts +15 -0
  24. package/dist/define/define.js +99 -0
  25. package/dist/discover/discover.d.ts +7 -0
  26. package/dist/discover/discover.js +99 -0
  27. package/dist/edges/files.d.ts +25 -0
  28. package/dist/edges/files.js +111 -0
  29. package/dist/edges/modules.d.ts +2 -0
  30. package/dist/edges/modules.js +6 -0
  31. package/dist/edges/process.d.ts +2 -0
  32. package/dist/edges/process.js +9 -0
  33. package/dist/edges/prompts.d.ts +2 -0
  34. package/dist/edges/prompts.js +14 -0
  35. package/dist/edges/system.d.ts +4 -0
  36. package/dist/edges/system.js +5 -0
  37. package/dist/errors/errors.d.ts +25 -0
  38. package/dist/errors/errors.js +37 -0
  39. package/dist/help/help.d.ts +15 -0
  40. package/dist/help/help.js +169 -0
  41. package/dist/index.d.ts +13 -0
  42. package/dist/index.js +12 -0
  43. package/dist/kits/terminal/constants.d.ts +10 -0
  44. package/dist/kits/terminal/constants.js +10 -0
  45. package/dist/kits/terminal/index.d.ts +3 -0
  46. package/dist/kits/terminal/index.js +2 -0
  47. package/dist/kits/terminal/models.d.ts +57 -0
  48. package/dist/kits/terminal/models.js +2 -0
  49. package/dist/kits/terminal/open-link.d.ts +2 -0
  50. package/dist/kits/terminal/open-link.js +12 -0
  51. package/dist/kits/terminal/open-tabs.d.ts +3 -0
  52. package/dist/kits/terminal/open-tabs.js +5 -0
  53. package/dist/kits/terminal/toml.d.ts +3 -0
  54. package/dist/kits/terminal/toml.js +53 -0
  55. package/dist/kits/terminal/warp.d.ts +6 -0
  56. package/dist/kits/terminal/warp.js +32 -0
  57. package/dist/kits/worktrees/constants.d.ts +25 -0
  58. package/dist/kits/worktrees/constants.js +25 -0
  59. package/dist/kits/worktrees/git.d.ts +2 -0
  60. package/dist/kits/worktrees/git.js +7 -0
  61. package/dist/kits/worktrees/index.d.ts +3 -0
  62. package/dist/kits/worktrees/index.js +2 -0
  63. package/dist/kits/worktrees/instances.d.ts +9 -0
  64. package/dist/kits/worktrees/instances.js +33 -0
  65. package/dist/kits/worktrees/lock-edges.d.ts +7 -0
  66. package/dist/kits/worktrees/lock-edges.js +21 -0
  67. package/dist/kits/worktrees/lock.d.ts +7 -0
  68. package/dist/kits/worktrees/lock.js +104 -0
  69. package/dist/kits/worktrees/models.d.ts +123 -0
  70. package/dist/kits/worktrees/models.js +2 -0
  71. package/dist/kits/worktrees/names.d.ts +5 -0
  72. package/dist/kits/worktrees/names.js +9 -0
  73. package/dist/kits/worktrees/queries.d.ts +20 -0
  74. package/dist/kits/worktrees/queries.js +61 -0
  75. package/dist/kits/worktrees/registration.d.ts +15 -0
  76. package/dist/kits/worktrees/registration.js +107 -0
  77. package/dist/kits/worktrees/registry-store.d.ts +3 -0
  78. package/dist/kits/worktrees/registry-store.js +27 -0
  79. package/dist/kits/worktrees/worktrees.d.ts +10 -0
  80. package/dist/kits/worktrees/worktrees.js +56 -0
  81. package/dist/models.d.ts +485 -0
  82. package/dist/models.js +1 -0
  83. package/dist/parse/parse.d.ts +8 -0
  84. package/dist/parse/parse.js +81 -0
  85. package/dist/resolve/resolve.d.ts +18 -0
  86. package/dist/resolve/resolve.js +96 -0
  87. package/dist/run/report.d.ts +6 -0
  88. package/dist/run/report.js +51 -0
  89. package/dist/run/run.d.ts +8 -0
  90. package/dist/run/run.js +163 -0
  91. package/dist/run/world.d.ts +3 -0
  92. package/dist/run/world.js +12 -0
  93. package/dist/utils/error-code.d.ts +3 -0
  94. package/dist/utils/error-code.js +7 -0
  95. package/dist/utils/error-message.d.ts +3 -0
  96. package/dist/utils/error-message.js +2 -0
  97. package/dist/utils/is-record.d.ts +3 -0
  98. package/dist/utils/is-record.js +2 -0
  99. package/dist/utils/tag.d.ts +3 -0
  100. package/dist/utils/tag.js +8 -0
  101. package/dist/utils/terminator.d.ts +5 -0
  102. package/dist/utils/terminator.js +8 -0
  103. package/package.json +72 -4
@@ -0,0 +1,485 @@
1
+ import type { ResolveHook } from 'node:module';
2
+ import type { ParseArgsConfig } from 'node:util';
3
+ import type { PromptObject } from 'prompts';
4
+ import type { EXIT_CODE, STYLE_CODES } from './constants.js';
5
+ import type { CommandError } from './errors/errors.js';
6
+ export type FlagDefinition = {
7
+ type: 'boolean' | 'string';
8
+ short?: string;
9
+ multiple?: boolean;
10
+ default?: boolean | string | readonly string[];
11
+ choices?: readonly string[];
12
+ description?: string;
13
+ };
14
+ export type FlagDefinitions = Record<string, FlagDefinition>;
15
+ /** What a command without `flags` declares. */
16
+ export type NoFlags = Record<never, FlagDefinition>;
17
+ export type ArgDefinition = {
18
+ name: string;
19
+ description?: string;
20
+ required?: boolean;
21
+ default?: string;
22
+ choices?: readonly string[];
23
+ };
24
+ export type ArgDefinitions = readonly ArgDefinition[];
25
+ /** The union of a definition's `choices`, or `string` when it has none. */
26
+ type ChoiceOf<TDefinition> = TDefinition extends {
27
+ readonly choices: readonly (infer TChoice extends string)[];
28
+ } ? TChoice : string;
29
+ type ArgValue<TArg extends ArgDefinition> = TArg extends {
30
+ readonly required: true;
31
+ } ? ChoiceOf<TArg> : TArg extends {
32
+ readonly default: string;
33
+ } ? ChoiceOf<TArg> : ChoiceOf<TArg> | undefined;
34
+ type FlagValue<TFlag extends FlagDefinition> = FlagDefinition extends TFlag ? unknown : TFlag extends {
35
+ readonly type: 'boolean';
36
+ } ? TFlag extends {
37
+ readonly default: boolean;
38
+ } ? boolean : boolean | undefined : TFlag extends {
39
+ readonly multiple: true;
40
+ } ? TFlag extends {
41
+ readonly default: readonly string[];
42
+ } ? ChoiceOf<TFlag>[] : ChoiceOf<TFlag>[] | undefined : TFlag extends {
43
+ readonly default: string;
44
+ } ? ChoiceOf<TFlag> : ChoiceOf<TFlag> | undefined;
45
+ /** A required or defaulted arg is a string; an optional one may be undefined; `choices` narrow it. */
46
+ export type InferArgs<TArgs extends ArgDefinitions> = {
47
+ [TArg in TArgs[number] as TArg['name']]: ArgValue<TArg>;
48
+ };
49
+ /** Boolean flags are booleans, string flags strings, `multiple` ones arrays; a default removes `undefined`. */
50
+ export type InferFlags<TFlags extends FlagDefinitions> = {
51
+ -readonly [TName in keyof TFlags]: FlagValue<TFlags[TName]>;
52
+ };
53
+ /** Color helpers; they return the text untouched when color is off. */
54
+ export type StyleFn = (text: unknown) => string;
55
+ export type StyleCodes = Record<string, number>;
56
+ export type Style = Record<keyof typeof STYLE_CODES, StyleFn>;
57
+ export type LogFn = (...parts: unknown[]) => void;
58
+ export type PromptOptions = {
59
+ hint?: string;
60
+ };
61
+ export type PromptFn = (questions: PromptObject | PromptObject[], options?: PromptOptions) => Promise<Record<string, unknown>>;
62
+ export type InvokeFn = (id: string, argv?: string[]) => Promise<unknown>;
63
+ /** What the prompts edge returns: the answers, and whether the user cancelled. */
64
+ export type PromptResult = {
65
+ answers: Record<string, unknown>;
66
+ cancelled: boolean;
67
+ };
68
+ export type AskFn = (questions: PromptObject | PromptObject[]) => Promise<PromptResult>;
69
+ /** The `ctx` a command receives. */
70
+ export type CommandContext = {
71
+ json: boolean;
72
+ interactive: boolean;
73
+ style: Style;
74
+ /** Human-readable output on stdout. Suppressed with --json. */
75
+ log: LogFn;
76
+ /** Progress and warnings on stderr; they never mix with --json output. */
77
+ warn: LogFn;
78
+ /** Asks the user, or throws a CommandError with a hint when nobody can answer. */
79
+ prompt: PromptFn;
80
+ /** Every discovered command. */
81
+ commands: CommandEntry[];
82
+ /** Runs another command by id or alias and returns its result. Inherits --json and --yes. */
83
+ invoke: InvokeFn;
84
+ };
85
+ /** Where output goes, and what the command was told about how it runs. */
86
+ export type ContextParts = {
87
+ io: IO;
88
+ global: GlobalFlags;
89
+ interactive: boolean;
90
+ style: Style;
91
+ registry: Registry;
92
+ invoke: InvokeFn;
93
+ };
94
+ export type CreateContextFn = (parts: ContextParts) => CommandContext;
95
+ export type RunInput<TArgs extends ArgDefinitions, TFlags extends FlagDefinitions> = {
96
+ args: InferArgs<TArgs>;
97
+ flags: InferFlags<TFlags>;
98
+ ctx: CommandContext;
99
+ };
100
+ export type RunFn<TArgs extends ArgDefinitions, TFlags extends FlagDefinitions> = (input: RunInput<TArgs, TFlags>) => unknown;
101
+ export type CommandDefinition<TArgs extends ArgDefinitions = ArgDefinitions, TFlags extends FlagDefinitions = FlagDefinitions> = {
102
+ description: string;
103
+ run: RunFn<TArgs, TFlags>;
104
+ args?: TArgs;
105
+ flags?: TFlags;
106
+ aliases?: readonly string[];
107
+ examples?: readonly string[];
108
+ [extra: string]: unknown;
109
+ };
110
+ /** What `defineCommand` returns: the definition with its defaults filled in. */
111
+ export type Command<TArgs extends ArgDefinitions = ArgDefinitions, TFlags extends FlagDefinitions = FlagDefinitions> = CommandDefinition<TArgs, TFlags> & {
112
+ args: TArgs;
113
+ flags: TFlags;
114
+ aliases: readonly string[];
115
+ examples: readonly string[];
116
+ };
117
+ /** What `defineTopic` takes: only the description is kept. */
118
+ export type TopicDefinition = {
119
+ description?: string;
120
+ };
121
+ /** What `defineTopic` returns. */
122
+ export type Topic = {
123
+ readonly description: string;
124
+ };
125
+ export type DefineCommandFn = <const TArgs extends ArgDefinitions = [], const TFlags extends FlagDefinitions = NoFlags>(definition: CommandDefinition<TArgs, TFlags>) => Command<TArgs, TFlags>;
126
+ /** Whether a value carries a `Symbol.for` tag (a definition's, or an error's), which survives two copies of the package. */
127
+ export type IsTaggedFn = (value: unknown, tag: symbol) => boolean;
128
+ export type IsCommandErrorFn = (value: unknown) => value is CommandErrorShape;
129
+ export type IsUsageErrorFn = (value: unknown) => value is UsageErrorShape;
130
+ export type DefineTopicFn = (definition?: TopicDefinition) => Topic;
131
+ export type IsCommandFn = (value: unknown) => value is Command;
132
+ export type IsTopicFn = (value: unknown) => value is Topic;
133
+ export type ExitCode = (typeof EXIT_CODE)[keyof typeof EXIT_CODE];
134
+ /** What a failure looks like to the reporter, whichever copy of Artemis threw it. */
135
+ export type CommandErrorShape = Error & {
136
+ hint: string | undefined;
137
+ exitCode: ExitCode;
138
+ };
139
+ export type UsageErrorShape = CommandErrorShape & {
140
+ usage: string | undefined;
141
+ };
142
+ export type CommandErrorOptions = {
143
+ hint?: string | undefined;
144
+ exitCode?: ExitCode;
145
+ cause?: unknown;
146
+ };
147
+ export type UsageErrorOptions = {
148
+ hint?: string | undefined;
149
+ usage?: string | undefined;
150
+ };
151
+ export type GlobalFlags = {
152
+ help: boolean;
153
+ json: boolean;
154
+ yes: boolean;
155
+ };
156
+ export type ParsedCommandLine = {
157
+ args: Record<string, string | undefined>;
158
+ flags: Record<string, unknown>;
159
+ global: GlobalFlags;
160
+ };
161
+ export type ParseTarget = Pick<Command, 'args' | 'flags'>;
162
+ export type ParseFn = (definition: ParseTarget, argv: readonly string[], usage: string) => ParsedCommandLine;
163
+ export type ParseOptions = NonNullable<ParseArgsConfig['options']>;
164
+ export type ParseOptionDescriptor = ParseOptions[string];
165
+ export type ParsedValues = Record<string, string | boolean | (string | boolean)[] | undefined>;
166
+ /** A value to check against the `choices` of the arg or flag it was given for. */
167
+ export type ChoiceCheck = {
168
+ kind: 'argument' | 'value for flag';
169
+ name: string;
170
+ value: unknown;
171
+ choices: readonly string[] | undefined;
172
+ usage: string;
173
+ };
174
+ export type CommandEntry = {
175
+ id: string;
176
+ file: string;
177
+ definition: Command;
178
+ };
179
+ export type TopicEntry = {
180
+ id: string;
181
+ description: string;
182
+ };
183
+ export type Registry = {
184
+ commands: Map<string, CommandEntry>;
185
+ topics: Map<string, TopicEntry>;
186
+ /** Alias -> command id. */
187
+ aliases: Map<string, string>;
188
+ warnings: string[];
189
+ };
190
+ export type DiscoverFn = (commandsDir: string) => Promise<Registry>;
191
+ /** What discovery needs to know about a folder entry. */
192
+ export type DirectoryEntry = {
193
+ name: string;
194
+ isDirectory: boolean;
195
+ };
196
+ /** Where discovery is: the registry being filled and the folder it started from. */
197
+ export type WalkContext = {
198
+ registry: Registry;
199
+ root: string;
200
+ };
201
+ export type ListDirectoryFn = (dir: string) => DirectoryEntry[];
202
+ export type PathExistsFn = (path: string) => boolean;
203
+ export type IsFolderFn = (path: string) => boolean;
204
+ export type MakeDirectoryFn = (dir: string) => void;
205
+ export type ReadTextIfExistsFn = (file: string) => string | undefined;
206
+ /** Creates the file only if it does not exist yet; false means someone else has it. */
207
+ export type CreateExclusiveFn = (file: string, text: string) => boolean;
208
+ /** When a file was last written, in ms since the epoch, or undefined when it is not there. */
209
+ export type ModifiedAtFn = (file: string) => number | undefined;
210
+ export type RealPathFn = (path: string) => string;
211
+ export type WorkingDirectoryFn = () => string;
212
+ export type ProcessIdFn = () => number;
213
+ export type ErrorCodeFn = (error: unknown) => string | undefined;
214
+ export type GitRootFn = (cwd: string) => string | null;
215
+ export type WriteTextFn = (file: string, text: string) => void;
216
+ export type RemoveFileFn = (file: string) => void;
217
+ export type HomeDirectoryFn = () => string;
218
+ export type ImportDefaultFn = (file: string) => Promise<unknown>;
219
+ export type MessageOfFn = (error: unknown) => string;
220
+ export type Resolution = {
221
+ kind: 'command';
222
+ entry: CommandEntry;
223
+ rest: string[];
224
+ } | {
225
+ kind: 'topic';
226
+ topic: TopicEntry;
227
+ rest: string[];
228
+ } | {
229
+ kind: 'unknown';
230
+ name: string;
231
+ };
232
+ export type ResolveFn = (registry: Registry, argv: readonly string[]) => Resolution;
233
+ /** The flags typed before the command name, and everything from the command name on. */
234
+ export type LeadingFlags = {
235
+ leading: string[];
236
+ rest: string[];
237
+ };
238
+ export type SplitLeadingFlagsFn = (argv: readonly string[]) => LeadingFlags;
239
+ export type IsGlobalFlagTokenFn = (token: string) => boolean;
240
+ export type BeforeTerminatorFn = (argv: readonly string[]) => string[];
241
+ export type JsonRequestedFn = (argv: readonly string[]) => boolean;
242
+ /** The exit code of a failure: the one a tagged error carries when it is a failure code, else 1. */
243
+ export type FailureCodeOfFn = (value: unknown) => ExitCode;
244
+ export type SuggestFn = (registry: Registry, name: string) => string | undefined;
245
+ export type FindCommandFn = (registry: Registry, id: string) => CommandEntry | undefined;
246
+ export type CreateStyleFn = (enabled: boolean) => Style;
247
+ /** A name and its explanation, one line of a help table. */
248
+ export type TableRow = readonly [name: string, text: string | undefined];
249
+ /** What a help listing shows: topics and the commands under them. */
250
+ export type HelpChildren = {
251
+ topics: TopicEntry[];
252
+ commands: CommandEntry[];
253
+ };
254
+ export type UsageFn = (bin: string, entry: CommandEntry) => string;
255
+ export type RootHelpFn = (bin: string, registry: Registry, style: Style) => string;
256
+ export type TopicHelpFn = (bin: string, registry: Registry, topic: TopicEntry, style: Style) => string;
257
+ export type CommandHelpFn = (bin: string, entry: CommandEntry, style: Style) => string;
258
+ export type ManifestArg = {
259
+ name: string;
260
+ description: string;
261
+ required: boolean;
262
+ default: string | null;
263
+ choices: readonly string[] | null;
264
+ };
265
+ export type ManifestFlag = {
266
+ name: string;
267
+ type: FlagDefinition['type'];
268
+ short: string | null;
269
+ multiple: boolean;
270
+ description: string;
271
+ default: boolean | string | readonly string[] | null;
272
+ choices: readonly string[] | null;
273
+ };
274
+ export type ManifestCommand = {
275
+ id: string;
276
+ description: string;
277
+ usage: string;
278
+ aliases: readonly string[];
279
+ args: ManifestArg[];
280
+ flags: ManifestFlag[];
281
+ examples: readonly string[];
282
+ };
283
+ /** Everything help shows, as data: what an agent reads to know which commands exist. */
284
+ export type Manifest = {
285
+ bin: string;
286
+ topics: TopicEntry[];
287
+ commands: ManifestCommand[];
288
+ globalFlags: ManifestFlag[];
289
+ };
290
+ export type ManifestFn = (bin: string, registry: Registry) => Manifest;
291
+ /** The manifest narrowed to one topic: the topic and its sub-topics, and every command under it. Same shape. */
292
+ export type TopicManifestFn = (everything: Manifest, topicId: string) => Manifest;
293
+ /** Where output goes. A node `Writable`, such as `process.stdout`, fits. */
294
+ export type OutputSink = {
295
+ write: (chunk: string) => unknown;
296
+ isTTY?: boolean;
297
+ };
298
+ export type IO = {
299
+ stdout: OutputSink;
300
+ stderr: OutputSink;
301
+ };
302
+ export type RunOptions = {
303
+ /** Name shown in help and usage lines. Required: a library does not know what it is called. */
304
+ bin: string;
305
+ /** Folder to discover commands in. */
306
+ commandsDir: string;
307
+ stdout?: OutputSink | undefined;
308
+ stderr?: OutputSink | undefined;
309
+ /** Whether prompts may be shown. Default: stdin and stdout are terminals. */
310
+ interactive?: boolean | undefined;
311
+ /** Default: stdout is a terminal and NO_COLOR is unset. */
312
+ color?: boolean | undefined;
313
+ env?: NodeJS.ProcessEnv | undefined;
314
+ };
315
+ export type RunCliFn = (argv: string[], options: RunOptions) => Promise<number>;
316
+ /** What the process offers when the caller did not say: its streams, environment and terminals. */
317
+ export type SystemProcess = {
318
+ stdout: OutputSink;
319
+ stderr: OutputSink;
320
+ env: NodeJS.ProcessEnv;
321
+ /** The command line, without the node and script words. */
322
+ args: string[];
323
+ stdinIsTTY: boolean;
324
+ stdoutIsTTY: boolean;
325
+ };
326
+ export type CurrentProcessFn = () => SystemProcess;
327
+ /** The options that describe the host a run happens in; `run()` and `cli()` both take them. */
328
+ export type HostOptions = Pick<RunOptions, 'stdout' | 'stderr' | 'interactive' | 'color' | 'env'>;
329
+ /** What a run resolved from the host options and the process, before any command is looked at. */
330
+ export type Environment = {
331
+ io: IO;
332
+ style: Style;
333
+ canPrompt: boolean;
334
+ };
335
+ export type World = Environment & {
336
+ bin: string;
337
+ };
338
+ export type ResolveWorldFn = (options: HostOptions) => Environment;
339
+ /** The id of the command a run is about, filled in as the command line is understood. */
340
+ export type RunState = {
341
+ commandId: string | undefined;
342
+ };
343
+ /** Everything the steps of one run share. */
344
+ export type Session = World & {
345
+ registry: Registry;
346
+ wantsJson: boolean;
347
+ state: RunState;
348
+ };
349
+ /** What help prints as text, and what it prints under --json. */
350
+ export type HelpOutput = {
351
+ text: string;
352
+ data: unknown;
353
+ };
354
+ /** An error as the user sees it. */
355
+ export type ErrorReport = {
356
+ message: string;
357
+ hint: string | undefined;
358
+ usage: string | undefined;
359
+ /** Only for a plain error, which means a bug. */
360
+ stack: string | undefined;
361
+ exitCode: ExitCode;
362
+ };
363
+ export type ReportContext = {
364
+ io: IO;
365
+ style: Style;
366
+ json: boolean;
367
+ commandId: string | undefined;
368
+ };
369
+ export type ReportErrorFn = (error: unknown, context: ReportContext) => number;
370
+ /** What a project may put under the "artemis" key of its package.json. Both fields are optional. */
371
+ export type ArtemisConfig = {
372
+ /** Relative to the folder of that package.json. Default "commands". */
373
+ commandsDir?: string;
374
+ /** The name help and usage lines show. */
375
+ bin?: string;
376
+ };
377
+ /** The folder holding the nearest qualifying package.json, and what that file says. */
378
+ export type Project = {
379
+ root: string;
380
+ manifest: Record<string, unknown>;
381
+ };
382
+ /** How a run got started: the shipped binary (`art`, `artemis`) or a project's own bin file. */
383
+ export type CliRoute = 'shipped' | 'own';
384
+ /** The settings a run uses, after defaults. `commandsDir` is absolute. */
385
+ export type CliSettings = {
386
+ bin: string;
387
+ commandsDir: string;
388
+ };
389
+ /** What a folder's package.json says: an object, or the error of a file that cannot be read or parsed. */
390
+ export type ManifestRead = {
391
+ manifest: Record<string, unknown>;
392
+ text: string;
393
+ } | {
394
+ error: CommandError;
395
+ };
396
+ /**
397
+ * Reads the package.json of a folder; undefined when the folder has none. An edge. It never throws
398
+ * for a bad file: it returns the error, and the search decides what to do with it.
399
+ */
400
+ export type ReadManifestFn = (dir: string) => ManifestRead | undefined;
401
+ /**
402
+ * Which package.json is the project, from positive signals only (nothing is inferred about what a
403
+ * file is not). Shipped route: the nearest one with an "artemis" key (declared, usually by `init`) or
404
+ * a commands folder next to it. Own route (`cli({ root })`): the nearest one with the identity of a
405
+ * package (a name, a `bin` or an "artemis" key), and never past the folder of an installed package,
406
+ * so a published tool cannot climb into the project that installed it. Markers and templates carry
407
+ * none of these signals. A package.json that cannot be read or parsed is an error, never skipped.
408
+ * Pure: the disk is only looked at through `readManifest` and `isFolder`.
409
+ */
410
+ export type FindProjectFn = (cwd: string, readManifest: ReadManifestFn, isFolder: IsFolderFn, route: CliRoute) => Project | undefined;
411
+ /** Throws a CommandError with a hint when the "artemis" key has a bad shape. */
412
+ export type ResolveSettingsFn = (project: Project, route: CliRoute) => CliSettings;
413
+ /**
414
+ * The name help and usage lines show. Both routes use the checked `"artemis".bin` first. The shipped
415
+ * binary then shows `art`, never the name of an unrelated CLI the project publishes. A project's own
416
+ * bin file (`cli({ root })`) shows the single key of its `bin` object, or the package name without
417
+ * its scope when `bin` is a string (npm names the command that way), and falls back to `art`.
418
+ */
419
+ export type ResolveBinNameFn = (config: ArtemisConfig, manifest: Record<string, unknown>, route: CliRoute) => string;
420
+ export type IsRecordFn = (value: unknown) => value is Record<string, unknown>;
421
+ /** What `cli()` takes: the host options of `run()`, plus where to look for the project. */
422
+ export type CliOptions = HostOptions & {
423
+ /** The command line, without the node and script words. Default: the arguments of this process. */
424
+ argv?: string[] | undefined;
425
+ /** Shipped route: where to start looking. Default: the current folder. */
426
+ cwd?: string | undefined;
427
+ /** Own route: where the bin file is (`import.meta.url`). The search starts from its folder. */
428
+ root?: string | URL | undefined;
429
+ };
430
+ export type CliFn = (options?: CliOptions) => Promise<number>;
431
+ /** Finds the settings for a folder, or throws a CommandError that says what to create or set. */
432
+ export type SettingsForFn = (start: string, route: CliRoute) => CliSettings;
433
+ /** The folder a `root` points at: itself when it is a folder, else the folder of the file. */
434
+ export type FolderOfRootFn = (root: string | URL) => string;
435
+ /** The module resolution hook that falls back to the running copy of the package. */
436
+ export type ResolveHookFn = ResolveHook;
437
+ /** What `init` needs to know about a folder; an edge reads it from the disk. */
438
+ export type InitInput = {
439
+ dir: string;
440
+ /** The text of the package.json, or undefined when there is none. */
441
+ manifestText: string | undefined;
442
+ manifest: Record<string, unknown> | undefined;
443
+ /** Where the commands go: the configured folder, or `commands`. Absolute. */
444
+ commandsDir: string;
445
+ commandsDirExists: boolean;
446
+ commandsDirIsEmpty: boolean;
447
+ /** Whether a command or topic named `init` is already there, which the `init` word would hide. */
448
+ shadowsCommand: boolean;
449
+ };
450
+ /** One thing `init` does. It never overwrites a file that exists. */
451
+ export type InitStep = {
452
+ kind: 'folder';
453
+ path: string;
454
+ }
455
+ /** `create` fails if the file appears in the meantime; `update` is the one deliberate rewrite, of package.json. */
456
+ | {
457
+ kind: 'file';
458
+ mode: 'create' | 'update';
459
+ path: string;
460
+ text: string;
461
+ };
462
+ export type InitPlan = {
463
+ steps: InitStep[];
464
+ /** What was already there and is left alone. */
465
+ kept: string[];
466
+ warnings: string[];
467
+ };
468
+ /** Pure: decides what to create, from what the folder already has. */
469
+ export type PlanInitFn = (input: InitInput) => InitPlan;
470
+ export type InitResult = {
471
+ root: string;
472
+ created: string[];
473
+ kept: string[];
474
+ warnings: string[];
475
+ };
476
+ /** Reads the folder, plans, and does it. Throws a CommandError for a package.json it cannot use. */
477
+ export type InitProjectFn = (dir: string) => InitResult;
478
+ export type DetectIndentFn = (text: string) => string;
479
+ /** The text of a JSON object with one more property at the end, and nothing else changed. */
480
+ export type AddKeyFn = (text: string, key: string, valueText: string) => string;
481
+ export type IsInstalledPackageRootFn = (dir: string) => boolean;
482
+ export type PackageNameForFn = (dir: string) => string;
483
+ /** `art init [folder] [--json]`. Returns the exit code. */
484
+ export type InitCommandFn = (words: string[], options: CliOptions, cwd: string) => number;
485
+ export {};
package/dist/models.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { ParseFn } from '../models.js';
2
+ /**
3
+ * Parses the words after the command name into its args and flags, plus the global flags.
4
+ * With --help nothing is validated, so help works even when required args are missing.
5
+ *
6
+ * @throws {UsageError} on unknown flags, missing or extra args, and values outside `choices`.
7
+ */
8
+ export declare const parse: ParseFn;
@@ -0,0 +1,81 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { ERROR_SENTENCE_SEPARATOR, GLOBAL_FLAGS } from '../constants.js';
3
+ import { UsageError } from '../errors/errors.js';
4
+ import { messageOf } from '../utils/error-message.js';
5
+ // parseArgs wants a mutable array for a repeatable flag's default
6
+ const toParseDefault = (value) => typeof value === 'object' ? [...value] : value;
7
+ function toParseOption(flag) {
8
+ return {
9
+ type: flag.type,
10
+ ...(flag.short ? { short: flag.short } : {}),
11
+ ...(flag.multiple ? { multiple: true } : {}),
12
+ ...(flag.default !== undefined ? { default: toParseDefault(flag.default) } : {})
13
+ };
14
+ }
15
+ const toParseOptions = (flags) => Object.fromEntries(Object.entries(flags).map(([name, flag]) => [name, toParseOption(flag)]));
16
+ // Node appends a paragraph about "--" to unknown option errors; the first sentence is the useful part
17
+ const firstSentence = (message) => message.split(ERROR_SENTENCE_SEPARATOR)[0] ?? message;
18
+ function parseCommandLine(argv, options, usage) {
19
+ try {
20
+ return parseArgs({ args: [...argv], options, allowPositionals: true, strict: true });
21
+ }
22
+ catch (error) {
23
+ throw new UsageError(firstSentence(messageOf(error)), { usage });
24
+ }
25
+ }
26
+ const readGlobalFlags = (values) => ({
27
+ help: Boolean(values.help),
28
+ json: Boolean(values.json),
29
+ yes: Boolean(values.yes)
30
+ });
31
+ function checkChoice({ kind, name, value, choices, usage }) {
32
+ const allowed = choices ?? [];
33
+ const isOutsideChoices = choices !== undefined && value !== undefined && !allowed.includes(value);
34
+ if (isOutsideChoices) {
35
+ throw new UsageError(`Invalid ${kind} "${name}": "${String(value)}". Allowed: ${choices.join(', ')}`, { usage });
36
+ }
37
+ }
38
+ function readArg(arg, positional, usage) {
39
+ const value = positional ?? arg.default;
40
+ if (value === undefined && arg.required) {
41
+ throw new UsageError(`Missing required argument "${arg.name}"`, { usage });
42
+ }
43
+ checkChoice({ kind: 'argument', name: arg.name, value, choices: arg.choices, usage });
44
+ return value;
45
+ }
46
+ function readArgs(args, positionals, usage) {
47
+ const extra = positionals[args.length];
48
+ if (extra !== undefined) {
49
+ throw new UsageError(`Unexpected argument "${extra}"`, { usage });
50
+ }
51
+ return Object.fromEntries(args.map((arg, index) => [arg.name, readArg(arg, positionals[index], usage)]));
52
+ }
53
+ function checkFlagChoices(definitions, values, usage) {
54
+ Object.entries(definitions).forEach(([name, flag]) => {
55
+ [values[name]].flat().forEach((value) => checkChoice({
56
+ kind: 'value for flag',
57
+ name: `--${name}`,
58
+ value,
59
+ choices: flag.choices,
60
+ usage
61
+ }));
62
+ });
63
+ }
64
+ /**
65
+ * Parses the words after the command name into its args and flags, plus the global flags.
66
+ * With --help nothing is validated, so help works even when required args are missing.
67
+ *
68
+ * @throws {UsageError} on unknown flags, missing or extra args, and values outside `choices`.
69
+ */
70
+ export const parse = (definition, argv, usage) => {
71
+ const options = toParseOptions({ ...definition.flags, ...GLOBAL_FLAGS });
72
+ const { values, positionals } = parseCommandLine(argv, options, usage);
73
+ const global = readGlobalFlags(values);
74
+ const flags = Object.fromEntries(Object.keys(definition.flags).map((name) => [name, values[name]]));
75
+ if (global.help) {
76
+ return { args: {}, flags, global };
77
+ }
78
+ const args = readArgs(definition.args, positionals, usage);
79
+ checkFlagChoices(definition.flags, values, usage);
80
+ return { args, flags, global };
81
+ };
@@ -0,0 +1,18 @@
1
+ import type { FindCommandFn, IsGlobalFlagTokenFn, ResolveFn, SplitLeadingFlagsFn, SuggestFn } from '../models.js';
2
+ /** The closest command, topic or alias name, if one is close enough to be a likely typo. */
3
+ export declare const suggest: SuggestFn;
4
+ /** A command by id or by alias. */
5
+ export declare const findCommand: FindCommandFn;
6
+ /**
7
+ * Works out which command (or topic) the command line names. Words are matched greedily, so
8
+ * `uve config set http://x` is the command `uve config set` with `http://x` left as its argument.
9
+ * `uve:config:set` and declared aliases are accepted as well, and a topic followed by `help` (`uve help`)
10
+ * shows that topic.
11
+ */
12
+ export declare const resolve: ResolveFn;
13
+ export declare const isGlobalFlagToken: IsGlobalFlagTokenFn;
14
+ /**
15
+ * Splits off the flags typed before the command name (`art --json math add 1 2`). They belong to
16
+ * the run, not to a command; `--` ends them like it ends any flags.
17
+ */
18
+ export declare const splitLeadingFlags: SplitLeadingFlagsFn;