@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,25 @@
1
+ import type { CommandErrorOptions, ExitCode, IsCommandErrorFn, IsUsageErrorFn, UsageErrorOptions } from '../models.js';
2
+ /**
3
+ * An expected failure: the message (and hint) is shown to the user, without a stack trace.
4
+ * Throw it from a command when something went wrong that the caller can fix.
5
+ */
6
+ export declare class CommandError extends Error {
7
+ /** How to fix the call, shown after the message. */
8
+ readonly hint: string | undefined;
9
+ /** The process exit code this failure maps to. */
10
+ readonly exitCode: ExitCode;
11
+ constructor(message: string, { hint, exitCode, cause }?: CommandErrorOptions);
12
+ }
13
+ /** The command line itself is wrong (unknown command, bad argument). Exits with 2. */
14
+ export declare class UsageError extends CommandError {
15
+ /** The usage line of the command, shown after the message. */
16
+ readonly usage: string | undefined;
17
+ constructor(message: string, { hint, usage }?: UsageErrorOptions);
18
+ }
19
+ /**
20
+ * Whether something is an expected failure (a `CommandError`), even one thrown by another copy of
21
+ * Artemis, where `instanceof` would say no and the hint and exit code would be lost.
22
+ */
23
+ export declare const isCommandError: IsCommandErrorFn;
24
+ /** Whether something is a wrong-command-line failure (a `UsageError`), from any copy of Artemis. */
25
+ export declare const isUsageError: IsUsageErrorFn;
@@ -0,0 +1,37 @@
1
+ import { COMMAND_ERROR_TAG, EXIT_CODE, USAGE_ERROR_TAG } from '../constants.js';
2
+ import { hasTag } from '../utils/tag.js';
3
+ /**
4
+ * An expected failure: the message (and hint) is shown to the user, without a stack trace.
5
+ * Throw it from a command when something went wrong that the caller can fix.
6
+ */
7
+ export class CommandError extends Error {
8
+ /** How to fix the call, shown after the message. */
9
+ hint;
10
+ /** The process exit code this failure maps to. */
11
+ exitCode;
12
+ constructor(message, { hint, exitCode = EXIT_CODE.FAILED, cause } = {}) {
13
+ super(message, { cause });
14
+ this.name = 'CommandError';
15
+ this.hint = hint;
16
+ this.exitCode = exitCode;
17
+ Object.defineProperty(this, COMMAND_ERROR_TAG, { value: true });
18
+ }
19
+ }
20
+ /** The command line itself is wrong (unknown command, bad argument). Exits with 2. */
21
+ export class UsageError extends CommandError {
22
+ /** The usage line of the command, shown after the message. */
23
+ usage;
24
+ constructor(message, { hint, usage } = {}) {
25
+ super(message, { hint, exitCode: EXIT_CODE.USAGE });
26
+ this.name = 'UsageError';
27
+ this.usage = usage;
28
+ Object.defineProperty(this, USAGE_ERROR_TAG, { value: true });
29
+ }
30
+ }
31
+ /**
32
+ * Whether something is an expected failure (a `CommandError`), even one thrown by another copy of
33
+ * Artemis, where `instanceof` would say no and the hint and exit code would be lost.
34
+ */
35
+ export const isCommandError = (value) => value instanceof Error && hasTag(value, COMMAND_ERROR_TAG);
36
+ /** Whether something is a wrong-command-line failure (a `UsageError`), from any copy of Artemis. */
37
+ export const isUsageError = (value) => isCommandError(value) && hasTag(value, USAGE_ERROR_TAG);
@@ -0,0 +1,15 @@
1
+ import type { CommandHelpFn, CreateStyleFn, ManifestFn, TopicManifestFn, RootHelpFn, TopicHelpFn, UsageFn } from '../models.js';
2
+ /** Color helpers; with `enabled` false they return the text untouched. */
3
+ export declare const createStyle: CreateStyleFn;
4
+ /** `art uve config set [url] [flags]` */
5
+ export declare const usage: UsageFn;
6
+ export declare const rootHelp: RootHelpFn;
7
+ export declare const topicHelp: TopicHelpFn;
8
+ export declare const commandHelp: CommandHelpFn;
9
+ /** Everything help shows, as data: what an agent reads to know which commands exist and how to call them. */
10
+ export declare const manifest: ManifestFn;
11
+ /**
12
+ * The manifest of one topic: the topic and its sub-topics, and every command under it. It keeps the
13
+ * shape of the whole manifest, so a caller reads both the same way.
14
+ */
15
+ export declare const topicManifest: TopicManifestFn;
@@ -0,0 +1,169 @@
1
+ import { ANSI_ESCAPE, ANSI_RESET, GLOBAL_FLAGS, ID_SEPARATOR, NOTE_SEPARATOR, SECTION_SEPARATOR, STYLE_CODES, TABLE_GAP, TABLE_INDENT } from '../constants.js';
2
+ const wrap = (code, enabled) => (text) => enabled
3
+ ? `${ANSI_ESCAPE}${code}m${String(text)}${ANSI_ESCAPE}${ANSI_RESET}m`
4
+ : String(text);
5
+ /** Color helpers; with `enabled` false they return the text untouched. */
6
+ export const createStyle = (enabled) => ({
7
+ bold: wrap(STYLE_CODES.bold, enabled),
8
+ dim: wrap(STYLE_CODES.dim, enabled),
9
+ red: wrap(STYLE_CODES.red, enabled),
10
+ green: wrap(STYLE_CODES.green, enabled),
11
+ yellow: wrap(STYLE_CODES.yellow, enabled),
12
+ cyan: wrap(STYLE_CODES.cyan, enabled)
13
+ });
14
+ const argToken = (arg) => (arg.required ? `<${arg.name}>` : `[${arg.name}]`);
15
+ /** `art uve config set [url] [flags]` */
16
+ export const usage = (bin, entry) => {
17
+ const { args, flags } = entry.definition;
18
+ const parts = [bin, entry.id, ...args.map(argToken)];
19
+ if (Object.keys(flags).length > 0) {
20
+ parts.push('[flags]');
21
+ }
22
+ return parts.join(ID_SEPARATOR);
23
+ };
24
+ function table(rows, style) {
25
+ const width = Math.max(0, ...rows.map(([name]) => name.length));
26
+ return rows
27
+ .map(([name, text]) => `${TABLE_INDENT}${style.green(name.padEnd(width))}${TABLE_GAP}${text ?? ''}`)
28
+ .join('\n');
29
+ }
30
+ const section = (title, body, style) => `${style.bold(title)}\n${body}`;
31
+ /** A description followed by its notes in parentheses, e.g. "Tags (repeatable; default: a)". */
32
+ const withNotes = (description, notes) => [description, notes.length ? `(${notes.join(NOTE_SEPARATOR)})` : ''].filter(Boolean).join(' ');
33
+ function flagNotes(flag) {
34
+ const notes = [];
35
+ if (flag.multiple) {
36
+ notes.push('repeatable');
37
+ }
38
+ if (flag.choices) {
39
+ notes.push(`one of: ${flag.choices.join(', ')}`);
40
+ }
41
+ if (flag.default !== undefined) {
42
+ notes.push(`default: ${String(flag.default)}`);
43
+ }
44
+ return notes;
45
+ }
46
+ function flagLabel(name, flag) {
47
+ const short = flag.short ? `-${flag.short}, ` : '';
48
+ const value = flag.type === 'string' ? ' <value>' : '';
49
+ return `${short}--${name}${value}`;
50
+ }
51
+ const flagRows = (flags) => Object.entries(flags).map(([name, flag]) => [
52
+ flagLabel(name, flag),
53
+ withNotes(flag.description, flagNotes(flag))
54
+ ]);
55
+ function argNotes(arg) {
56
+ const notes = [];
57
+ if (arg.required) {
58
+ notes.push('required');
59
+ }
60
+ else if (arg.default !== undefined) {
61
+ notes.push(`default: ${arg.default}`);
62
+ }
63
+ if (arg.choices) {
64
+ notes.push(`one of: ${arg.choices.join(', ')}`);
65
+ }
66
+ return notes;
67
+ }
68
+ const argRows = (args) => args.map((arg) => [arg.name, withNotes(arg.description, argNotes(arg))]);
69
+ const globalFlagsSection = (style) => section('Global flags', table(flagRows(GLOBAL_FLAGS), style), style);
70
+ const isTopLevel = (id) => !id.includes(ID_SEPARATOR);
71
+ // The root lists top-level topics and commands; a topic lists every command under it, however
72
+ // deep, so "art uve" shows "uve config set" rather than an intermediate "uve config" topic.
73
+ function childrenOf(registry, prefix) {
74
+ const commands = [...registry.commands.values()];
75
+ if (!prefix) {
76
+ return {
77
+ topics: [...registry.topics.values()].filter((topic) => isTopLevel(topic.id)),
78
+ commands: commands.filter((entry) => isTopLevel(entry.id))
79
+ };
80
+ }
81
+ return {
82
+ topics: [],
83
+ commands: commands.filter((entry) => entry.id.startsWith(`${prefix}${ID_SEPARATOR}`))
84
+ };
85
+ }
86
+ function listing(registry, prefix, style) {
87
+ const { topics, commands } = childrenOf(registry, prefix);
88
+ const sections = [];
89
+ if (topics.length) {
90
+ const rows = topics.map((topic) => [topic.id, topic.description]);
91
+ sections.push(section('Topics', table(rows, style), style));
92
+ }
93
+ if (commands.length) {
94
+ const rows = commands.map((entry) => [entry.id, entry.definition.description]);
95
+ sections.push(section('Commands', table(rows, style), style));
96
+ }
97
+ sections.push(globalFlagsSection(style));
98
+ return sections.join(SECTION_SEPARATOR);
99
+ }
100
+ export const rootHelp = (bin, registry, style) => [
101
+ `${style.bold('Usage')} ${bin} <topic> <command> [args] [flags]`,
102
+ listing(registry, '', style),
103
+ style.dim(`Run "${bin} <topic>" to list a topic, or "${bin} <command> --help" for details.`)
104
+ ].join(SECTION_SEPARATOR);
105
+ export const topicHelp = (_bin, registry, topic, style) => [
106
+ `${style.bold(topic.id)}${topic.description ? ` ${topic.description}` : ''}`,
107
+ listing(registry, topic.id, style)
108
+ ].join(SECTION_SEPARATOR);
109
+ export const commandHelp = (bin, entry, style) => {
110
+ const { description, args, flags, aliases, examples } = entry.definition;
111
+ const sections = [description, `${style.bold('Usage')} ${usage(bin, entry)}`];
112
+ if (args.length) {
113
+ sections.push(section('Arguments', table(argRows(args), style), style));
114
+ }
115
+ if (Object.keys(flags).length) {
116
+ sections.push(section('Flags', table(flagRows(flags), style), style));
117
+ }
118
+ sections.push(globalFlagsSection(style));
119
+ if (aliases.length) {
120
+ sections.push(`${style.bold('Aliases')} ${aliases.join(', ')}`);
121
+ }
122
+ if (examples.length) {
123
+ const lines = examples.map((example) => `${TABLE_INDENT}${bin} ${example}`);
124
+ sections.push(section('Examples', lines.join('\n'), style));
125
+ }
126
+ return sections.join(SECTION_SEPARATOR);
127
+ };
128
+ const describeFlags = (flags) => Object.entries(flags).map(([name, flag]) => ({
129
+ name,
130
+ type: flag.type,
131
+ short: flag.short ?? null,
132
+ multiple: Boolean(flag.multiple),
133
+ description: flag.description ?? '',
134
+ default: flag.default ?? null,
135
+ choices: flag.choices ?? null
136
+ }));
137
+ const describeArgs = (args) => args.map((arg) => ({
138
+ name: arg.name,
139
+ description: arg.description ?? '',
140
+ required: Boolean(arg.required),
141
+ default: arg.default ?? null,
142
+ choices: arg.choices ?? null
143
+ }));
144
+ const describeCommand = (bin, entry) => ({
145
+ id: entry.id,
146
+ description: entry.definition.description,
147
+ usage: usage(bin, entry),
148
+ aliases: entry.definition.aliases,
149
+ args: describeArgs(entry.definition.args),
150
+ flags: describeFlags(entry.definition.flags),
151
+ examples: entry.definition.examples
152
+ });
153
+ /** Everything help shows, as data: what an agent reads to know which commands exist and how to call them. */
154
+ export const manifest = (bin, registry) => ({
155
+ bin,
156
+ topics: [...registry.topics.values()],
157
+ commands: [...registry.commands.values()].map((entry) => describeCommand(bin, entry)),
158
+ globalFlags: describeFlags(GLOBAL_FLAGS)
159
+ });
160
+ const isWithin = (id, topicId) => id === topicId || id.startsWith(`${topicId} `);
161
+ /**
162
+ * The manifest of one topic: the topic and its sub-topics, and every command under it. It keeps the
163
+ * shape of the whole manifest, so a caller reads both the same way.
164
+ */
165
+ export const topicManifest = (everything, topicId) => ({
166
+ ...everything,
167
+ topics: everything.topics.filter((topic) => isWithin(topic.id, topicId)),
168
+ commands: everything.commands.filter((command) => isWithin(command.id, topicId))
169
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Artemis: drop a file in a folder, get a command.
3
+ *
4
+ * commands/uve/config/set.js -> art uve config set
5
+ *
6
+ * Folders are topics, `_topic.js` describes a topic, and anything starting with "_" is private.
7
+ * Every command gets --help, --json and --yes, and `art help --json` describes all of them.
8
+ */
9
+ export { EXIT_CODE } from './constants.js';
10
+ export { defineCommand, defineTopic } from './define/define.js';
11
+ export { CommandError, UsageError } from './errors/errors.js';
12
+ export type { ArgDefinition, Command, CommandContext, CommandDefinition, ExitCode, FlagDefinition, InferArgs, InferFlags, RunInput, RunOptions, TopicDefinition } from './models.js';
13
+ export { run } from './run/run.js';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Artemis: drop a file in a folder, get a command.
3
+ *
4
+ * commands/uve/config/set.js -> art uve config set
5
+ *
6
+ * Folders are topics, `_topic.js` describes a topic, and anything starting with "_" is private.
7
+ * Every command gets --help, --json and --yes, and `art help --json` describes all of them.
8
+ */
9
+ export { EXIT_CODE } from './constants.js';
10
+ export { defineCommand, defineTopic } from './define/define.js';
11
+ export { CommandError, UsageError } from './errors/errors.js';
12
+ export { run } from './run/run.js';
@@ -0,0 +1,10 @@
1
+ export declare const WARP_CONFIG_FOLDER = ".warp/tab_configs";
2
+ export declare const WARP_LINK_PREFIX = "warp://tab_config/";
3
+ export declare const TAB_CONFIG_EXTENSION = ".toml";
4
+ export declare const OPEN_COMMAND = "open";
5
+ export declare const ROOT_PANE_ID = "root";
6
+ export declare const FIRST_PANE_ID = "pane_1";
7
+ export declare const PANE_ID_PREFIX = "pane_";
8
+ export declare const SPLIT_ID_PREFIX = "split_";
9
+ /** A tab id becomes a file name and part of a link, so it is a plain word: no slashes, spaces or `#`. */
10
+ export declare const TAB_ID_PATTERN: RegExp;
@@ -0,0 +1,10 @@
1
+ export const WARP_CONFIG_FOLDER = '.warp/tab_configs';
2
+ export const WARP_LINK_PREFIX = 'warp://tab_config/';
3
+ export const TAB_CONFIG_EXTENSION = '.toml';
4
+ export const OPEN_COMMAND = 'open';
5
+ export const ROOT_PANE_ID = 'root';
6
+ export const FIRST_PANE_ID = 'pane_1';
7
+ export const PANE_ID_PREFIX = 'pane_';
8
+ export const SPLIT_ID_PREFIX = 'split_';
9
+ /** A tab id becomes a file name and part of a link, so it is a plain word: no slashes, spaces or `#`. */
10
+ export const TAB_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
@@ -0,0 +1,3 @@
1
+ export type { OpenLinkFn, SplitDirection, Terminal, TerminalLayout, TerminalPane, TerminalSplit, TerminalTab, WarpOptions } from './models.js';
2
+ export { openTabs } from './open-tabs.js';
3
+ export { warp } from './warp.js';
@@ -0,0 +1,2 @@
1
+ export { openTabs } from './open-tabs.js';
2
+ export { warp } from './warp.js';
@@ -0,0 +1,57 @@
1
+ /** One terminal pane: a folder, the commands to type in it, and whether it takes focus. */
2
+ export type TerminalPane = {
3
+ dir: string;
4
+ run?: readonly string[];
5
+ focus?: boolean;
6
+ };
7
+ /** `horizontal` puts children side by side, `vertical` stacks them. */
8
+ export type SplitDirection = 'horizontal' | 'vertical';
9
+ export type TerminalSplit = {
10
+ split: SplitDirection;
11
+ children: readonly TerminalLayout[];
12
+ };
13
+ export type TerminalLayout = TerminalPane | TerminalSplit;
14
+ type TabBase = {
15
+ id: string;
16
+ name: string;
17
+ title?: string;
18
+ color?: string;
19
+ };
20
+ /** A tab is either a flat list of panes (left to right) or a layout tree of splits. */
21
+ export type TerminalTab = (TabBase & {
22
+ panes: readonly TerminalPane[];
23
+ layout?: never;
24
+ }) | (TabBase & {
25
+ layout: TerminalLayout;
26
+ panes?: never;
27
+ });
28
+ /** What an adapter does with a tab: write its config, open it, remove it. */
29
+ export type Terminal = {
30
+ write: (tab: TerminalTab) => void;
31
+ open: (id: string) => void;
32
+ remove: (id: string) => void;
33
+ };
34
+ export type OpenLinkFn = (url: string) => void;
35
+ export type WarpOptions = {
36
+ /** Where Warp reads Tab Configs from. Default: `~/.warp/tab_configs`. */
37
+ configDir?: string;
38
+ /** How a link is opened. Default: macOS `open`. */
39
+ open?: OpenLinkFn;
40
+ };
41
+ export type CheckTabIdFn = (id: string) => string;
42
+ export type WarpFn = (options?: WarpOptions) => Terminal;
43
+ export type OpenTabsFn = (terminal: Terminal, tabs: readonly TerminalTab[]) => void;
44
+ /** A pane or split with the id Warp refers to it by. */
45
+ export type IdentifiedNode = (TerminalPane & {
46
+ id: string;
47
+ }) | {
48
+ split: SplitDirection;
49
+ id: string;
50
+ children: IdentifiedNode[];
51
+ };
52
+ export type IdCounters = {
53
+ pane: number;
54
+ split: number;
55
+ };
56
+ export type ToTomlFn = (tab: TerminalTab) => string;
57
+ export {};
@@ -0,0 +1,2 @@
1
+ // Types of the terminal kit: no type lives in a logic file.
2
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { OpenLinkFn } from './models.js';
2
+ export declare const openLink: OpenLinkFn;
@@ -0,0 +1,12 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { OPEN_COMMAND } from './constants.js';
3
+ import { CommandError } from '../../errors/errors.js';
4
+ // Edge: opens a link with the macOS `open` command.
5
+ export const openLink = (url) => {
6
+ const result = spawnSync(OPEN_COMMAND, [url], { encoding: 'utf8' });
7
+ if (result.status !== 0) {
8
+ throw new CommandError(`Warp did not open ${url}: ${String(result.stderr ?? '').trim()}`, {
9
+ hint: 'Check Warp is installed and handles warp:// links.'
10
+ });
11
+ }
12
+ };
@@ -0,0 +1,3 @@
1
+ import type { OpenTabsFn } from './models.js';
2
+ /** Writes every tab, then opens them in order. */
3
+ export declare const openTabs: OpenTabsFn;
@@ -0,0 +1,5 @@
1
+ /** Writes every tab, then opens them in order. */
2
+ export const openTabs = (terminal, tabs) => {
3
+ tabs.forEach((tab) => terminal.write(tab));
4
+ tabs.forEach((tab) => terminal.open(tab.id));
5
+ };
@@ -0,0 +1,3 @@
1
+ import type { ToTomlFn } from './models.js';
2
+ /** The Warp Tab Config (TOML) for a tab. Pure: the same tab always gives the same text. */
3
+ export declare const toToml: ToTomlFn;
@@ -0,0 +1,53 @@
1
+ import { FIRST_PANE_ID, PANE_ID_PREFIX, ROOT_PANE_ID, SPLIT_ID_PREFIX } from './constants.js';
2
+ // TOML basic strings use the same escapes as JSON for what we write (paths and commands)
3
+ const str = (value) => JSON.stringify(value);
4
+ const isSplit = (node) => 'split' in node;
5
+ /** A flat list of panes is a left-to-right split; a single pane needs no split at all. */
6
+ function layoutOf(tab) {
7
+ if (tab.layout) {
8
+ return tab.layout;
9
+ }
10
+ const panes = tab.panes ?? [];
11
+ const [only] = panes;
12
+ return panes.length === 1 && only ? only : { split: 'horizontal', children: panes };
13
+ }
14
+ /** Assigns Warp pane ids: "root" for the top node, split_N for nested splits, pane_N for panes. */
15
+ function assignIds(node, counters, isRoot) {
16
+ if (!isSplit(node)) {
17
+ return { ...node, id: isRoot ? FIRST_PANE_ID : `${PANE_ID_PREFIX}${++counters.pane}` };
18
+ }
19
+ const id = isRoot ? ROOT_PANE_ID : `${SPLIT_ID_PREFIX}${++counters.split}`;
20
+ const children = node.children.map((child) => assignIds(child, counters, false));
21
+ return { split: node.split, id, children };
22
+ }
23
+ // Warp reads the first [[panes]] entry as the root, so nodes go out parent first
24
+ function nodeLines(node) {
25
+ const header = ['', '[[panes]]', `id = ${str(node.id)}`];
26
+ if ('children' in node) {
27
+ const ids = node.children.map((child) => str(child.id)).join(', ');
28
+ return [
29
+ ...header,
30
+ `split = ${str(node.split)}`,
31
+ `children = [${ids}]`,
32
+ ...node.children.flatMap(nodeLines)
33
+ ];
34
+ }
35
+ return [
36
+ ...header,
37
+ 'type = "terminal"',
38
+ `directory = ${str(node.dir)}`,
39
+ ...(node.run?.length ? [`commands = [${node.run.map(str).join(', ')}]`] : []),
40
+ ...(node.focus ? ['is_focused = true'] : [])
41
+ ];
42
+ }
43
+ /** The Warp Tab Config (TOML) for a tab. Pure: the same tab always gives the same text. */
44
+ export const toToml = (tab) => {
45
+ const root = assignIds(layoutOf(tab), { pane: 0, split: 0 }, true);
46
+ const lines = [
47
+ `name = ${str(tab.name)}`,
48
+ ...(tab.title ? [`title = ${str(tab.title)}`] : []),
49
+ ...(tab.color ? [`color = ${str(tab.color)}`] : []),
50
+ ...nodeLines(root)
51
+ ];
52
+ return `${lines.join('\n')}\n`;
53
+ };
@@ -0,0 +1,6 @@
1
+ import type { WarpFn } from './models.js';
2
+ /**
3
+ * Warp adapter: each tab becomes a Tab Config (`<id>.toml`) opened through its warp:// link, as a
4
+ * new tab in the focused window (or a new window if none is open).
5
+ */
6
+ export declare const warp: WarpFn;
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+ import { makeDirectory, removeFile, writeText } from '../../edges/files.js';
3
+ import { homeDirectory } from '../../edges/system.js';
4
+ import { CommandError } from '../../errors/errors.js';
5
+ import { TAB_CONFIG_EXTENSION, TAB_ID_PATTERN, WARP_CONFIG_FOLDER, WARP_LINK_PREFIX } from './constants.js';
6
+ import { openLink } from './open-link.js';
7
+ import { toToml } from './toml.js';
8
+ /** A tab id is used as a file name and in a link, so anything but a plain word is refused. */
9
+ const checkedId = (id) => {
10
+ if (!TAB_ID_PATTERN.test(id)) {
11
+ throw new CommandError(`"${id}" is not a valid tab id.`, {
12
+ hint: 'Use letters, digits, dashes and underscores only, e.g. "demo_dotcms".'
13
+ });
14
+ }
15
+ return id;
16
+ };
17
+ /**
18
+ * Warp adapter: each tab becomes a Tab Config (`<id>.toml`) opened through its warp:// link, as a
19
+ * new tab in the focused window (or a new window if none is open).
20
+ */
21
+ export const warp = ({ configDir = path.join(homeDirectory(), WARP_CONFIG_FOLDER), open = openLink } = {}) => {
22
+ const fileOf = (id) => path.join(configDir, `${checkedId(id)}${TAB_CONFIG_EXTENSION}`);
23
+ return {
24
+ write: (tab) => {
25
+ const file = fileOf(tab.id);
26
+ makeDirectory(configDir);
27
+ writeText(file, toToml(tab));
28
+ },
29
+ open: (id) => open(`${WARP_LINK_PREFIX}${checkedId(id)}`),
30
+ remove: (id) => removeFile(fileOf(id))
31
+ };
32
+ };
@@ -0,0 +1,25 @@
1
+ /** The main checkout is slot 0 and needs no registration; registered worktrees start at 1. */
2
+ export declare const MAIN_SLOT = 0;
3
+ export declare const FIRST_WORKTREE_SLOT = 1;
4
+ export declare const DEFAULT_MAIN_NAME = "main";
5
+ export declare const DEFAULT_SLOT_STEP = 100;
6
+ export declare const DEFAULT_COLORS: readonly ["blue", "green", "magenta", "cyan", "yellow", "red"];
7
+ /** An instance name is a lowercase letter or digit, then letters, digits and dashes. */
8
+ export declare const MAX_INSTANCE_NAME_LENGTH = 31;
9
+ export declare const VALID_NAME: RegExp;
10
+ export declare const INVALID_NAME_RUN: RegExp;
11
+ export declare const EDGE_DASHES: RegExp;
12
+ export declare const FIRST_SUFFIX = 2;
13
+ export declare const FALLBACK_WORKTREE_NAME = "worktree";
14
+ export declare const STATE_INDENT_SPACES = 4;
15
+ export declare const LOCK_SUFFIX = ".lock";
16
+ export declare const LOCK_TIMEOUT_MS = 10000;
17
+ export declare const LOCK_POLL_MS = 10;
18
+ /** A lock file with no process id in it is probably being written; it is left alone this long. */
19
+ export declare const LOCK_GRACE_MS = 200;
20
+ /** Whoever is taking over a stale lock is given this long before the takeover itself counts as abandoned. */
21
+ export declare const TAKEOVER_STALE_MS = 5000;
22
+ export declare const TAKEOVER_SUFFIX = ".takeover";
23
+ export declare const INT32_BYTES = 4;
24
+ /** Signal 0 only asks whether the process exists. */
25
+ export declare const PROBE_SIGNAL = 0;
@@ -0,0 +1,25 @@
1
+ /** The main checkout is slot 0 and needs no registration; registered worktrees start at 1. */
2
+ export const MAIN_SLOT = 0;
3
+ export const FIRST_WORKTREE_SLOT = 1;
4
+ export const DEFAULT_MAIN_NAME = 'main';
5
+ export const DEFAULT_SLOT_STEP = 100;
6
+ export const DEFAULT_COLORS = ['blue', 'green', 'magenta', 'cyan', 'yellow', 'red'];
7
+ /** An instance name is a lowercase letter or digit, then letters, digits and dashes. */
8
+ export const MAX_INSTANCE_NAME_LENGTH = 31;
9
+ export const VALID_NAME = new RegExp(`^[a-z0-9][a-z0-9-]{0,${MAX_INSTANCE_NAME_LENGTH - 1}}$`);
10
+ export const INVALID_NAME_RUN = /[^a-z0-9-]+/g;
11
+ export const EDGE_DASHES = /^-+|-+$/g;
12
+ export const FIRST_SUFFIX = 2;
13
+ export const FALLBACK_WORKTREE_NAME = 'worktree';
14
+ export const STATE_INDENT_SPACES = 4;
15
+ export const LOCK_SUFFIX = '.lock';
16
+ export const LOCK_TIMEOUT_MS = 10_000;
17
+ export const LOCK_POLL_MS = 10;
18
+ /** A lock file with no process id in it is probably being written; it is left alone this long. */
19
+ export const LOCK_GRACE_MS = 200;
20
+ /** Whoever is taking over a stale lock is given this long before the takeover itself counts as abandoned. */
21
+ export const TAKEOVER_STALE_MS = 5000;
22
+ export const TAKEOVER_SUFFIX = '.takeover';
23
+ export const INT32_BYTES = 4;
24
+ /** Signal 0 only asks whether the process exists. */
25
+ export const PROBE_SIGNAL = 0;
@@ -0,0 +1,2 @@
1
+ import type { GitRootFn } from '../../models.js';
2
+ export declare const gitRoot: GitRootFn;
@@ -0,0 +1,7 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { realPathOrResolve } from '../../edges/files.js';
3
+ // Edge: asks git which checkout a folder belongs to.
4
+ export const gitRoot = (cwd) => {
5
+ const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' });
6
+ return result.status === 0 ? realPathOrResolve(result.stdout.trim()) : null;
7
+ };
@@ -0,0 +1,3 @@
1
+ export type { CurrentOptions, EnsureDetachedOptions, EnsureOptions, IsCheckoutFn, WorktreeInstance, WorktreeMeta, Worktrees, WorktreesOptions } from './models.js';
2
+ export { toWorktreeName } from './names.js';
3
+ export { defineWorktrees } from './worktrees.js';
@@ -0,0 +1,2 @@
1
+ export { toWorktreeName } from './names.js';
2
+ export { defineWorktrees } from './worktrees.js';
@@ -0,0 +1,9 @@
1
+ import type { DescribeInstanceFn, NextFreeSlotFn, RecordOfFn, UniqueNameFn } from './models.js';
2
+ /** A record of the registry as an instance: its ports shifted by slot, and its color by slot. */
3
+ export declare const describeInstance: DescribeInstanceFn;
4
+ /** The record of a registered name. Only the registry's own entries count, never `constructor` and friends. */
5
+ export declare const recordOf: RecordOfFn;
6
+ /** The lowest slot no registered worktree uses. Slot 0 is the main checkout. */
7
+ export declare const nextFreeSlot: NextFreeSlotFn;
8
+ /** `wanted`, or `base-2`, `base-3`... when it is taken. */
9
+ export declare const uniqueName: UniqueNameFn;
@@ -0,0 +1,33 @@
1
+ import { FIRST_SUFFIX, FIRST_WORKTREE_SLOT, MAIN_SLOT } from './constants.js';
2
+ /** A record of the registry as an instance: its ports shifted by slot, and its color by slot. */
3
+ export const describeInstance = (config, name, record) => ({
4
+ name,
5
+ slot: record.slot,
6
+ root: record.root,
7
+ isMain: record.slot === MAIN_SLOT,
8
+ meta: record.meta ?? {},
9
+ color: config.colors[record.slot % config.colors.length] ?? '',
10
+ ports: Object.fromEntries(Object.entries(config.ports).map(([key, port]) => [
11
+ key,
12
+ port + record.slot * config.slotStep
13
+ ]))
14
+ });
15
+ /** The record of a registered name. Only the registry's own entries count, never `constructor` and friends. */
16
+ export const recordOf = (records, name) => Object.hasOwn(records, name) ? records[name] : undefined;
17
+ /** The lowest slot no registered worktree uses. Slot 0 is the main checkout. */
18
+ export const nextFreeSlot = (records) => {
19
+ const used = new Set(Object.values(records).map((record) => record.slot));
20
+ let slot = FIRST_WORKTREE_SLOT;
21
+ while (used.has(slot)) {
22
+ slot++;
23
+ }
24
+ return slot;
25
+ };
26
+ /** `wanted`, or `base-2`, `base-3`... when it is taken. */
27
+ export const uniqueName = (wanted, base, isTaken) => {
28
+ let name = wanted;
29
+ for (let suffix = FIRST_SUFFIX; isTaken(name); suffix++) {
30
+ name = `${base}-${suffix}`;
31
+ }
32
+ return name;
33
+ };
@@ -0,0 +1,7 @@
1
+ import type { IsProcessAliveFn, NowFn, PauseFn, TokenFn } from './models.js';
2
+ export declare const now: NowFn;
3
+ /** Signal 0 only asks whether the process exists. Someone else's process (EPERM) is alive too. */
4
+ export declare const isProcessAlive: IsProcessAliveFn;
5
+ /** A blocking wait: the kit is synchronous like the rest of its callers. */
6
+ export declare const pause: PauseFn;
7
+ export declare const newToken: TokenFn;
@@ -0,0 +1,21 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { ERROR_CODE } from '../../constants.js';
3
+ import { INT32_BYTES, PROBE_SIGNAL } from './constants.js';
4
+ import { codeOf } from '../../utils/error-code.js';
5
+ // Edges of the registry lock: the clock, process liveness, and waiting.
6
+ export const now = () => Date.now();
7
+ /** Signal 0 only asks whether the process exists. Someone else's process (EPERM) is alive too. */
8
+ export const isProcessAlive = (pid) => {
9
+ try {
10
+ process.kill(pid, PROBE_SIGNAL);
11
+ return true;
12
+ }
13
+ catch (error) {
14
+ return codeOf(error) === ERROR_CODE.PERMISSION_DENIED;
15
+ }
16
+ };
17
+ /** A blocking wait: the kit is synchronous like the rest of its callers. */
18
+ export const pause = (ms) => {
19
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(INT32_BYTES)), 0, 0, ms);
20
+ };
21
+ export const newToken = () => randomUUID();