@cmodjs/core 0.1.7 → 0.1.9

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 (87) hide show
  1. package/README.md +2 -2
  2. package/docs/commands.md +14 -14
  3. package/docs/dependencies.md +1 -1
  4. package/docs/hooks.md +69 -13
  5. package/docs/index.md +7 -5
  6. package/docs/install-steps.md +19 -19
  7. package/docs/jobs.md +22 -21
  8. package/docs/mod.md +44 -12
  9. package/docs/state.md +8 -8
  10. package/docs/testing.md +25 -9
  11. package/docs/ui.md +33 -9
  12. package/jobs/check.js +2 -2
  13. package/jobs/context.d.ts.map +1 -1
  14. package/jobs/context.js +2 -0
  15. package/jobs/permissions/decide-permission.js +2 -2
  16. package/jobs/permissions/find-project-scope.d.ts +1 -1
  17. package/jobs/permissions/find-project-scope.d.ts.map +1 -1
  18. package/jobs/permissions/find-project-scope.js +9 -9
  19. package/jobs/permissions/match-target.d.ts.map +1 -1
  20. package/jobs/permissions/match-target.js +4 -3
  21. package/mod.d.ts +15 -5
  22. package/mod.d.ts.map +1 -1
  23. package/package.json +2 -1
  24. package/path.d.ts +2 -0
  25. package/path.d.ts.map +1 -0
  26. package/path.js +1 -0
  27. package/records.js +3 -3
  28. package/register.d.ts.map +1 -1
  29. package/register.js +8 -2
  30. package/runtime/claude.d.ts +4 -1
  31. package/runtime/claude.d.ts.map +1 -1
  32. package/runtime/hooks.d.ts +10 -2
  33. package/runtime/hooks.d.ts.map +1 -1
  34. package/runtime/hooks.js +56 -18
  35. package/runtime/lifecycle.d.ts.map +1 -1
  36. package/runtime/lifecycle.js +17 -7
  37. package/runtime/ui.d.ts.map +1 -1
  38. package/runtime/ui.js +38 -8
  39. package/shell.d.ts +2 -0
  40. package/shell.d.ts.map +1 -0
  41. package/shell.js +1 -0
  42. package/src/jobs/check.ts +2 -2
  43. package/src/jobs/context.ts +2 -0
  44. package/src/jobs/permissions/decide-permission.ts +2 -2
  45. package/src/jobs/permissions/find-project-scope.ts +11 -10
  46. package/src/jobs/permissions/match-target.ts +4 -3
  47. package/src/mod.ts +22 -3
  48. package/src/path.ts +1 -0
  49. package/src/records.ts +3 -3
  50. package/src/register.ts +8 -2
  51. package/src/runtime/claude.ts +11 -1
  52. package/src/runtime/hooks.ts +62 -23
  53. package/src/runtime/lifecycle.ts +16 -9
  54. package/src/runtime/ui.ts +35 -8
  55. package/src/shell.ts +1 -0
  56. package/src/testing/fake-claude.ts +14 -5
  57. package/src/testing/fake-elements.ts +5 -7
  58. package/src/testing/fake-files.ts +5 -5
  59. package/src/testing.ts +83 -11
  60. package/src/ui/define-pane.ts +5 -1
  61. package/src/utils/call-effects.ts +12 -11
  62. package/src/utils/parse-shell.ts +28 -36
  63. package/src/utils/paths.ts +4 -19
  64. package/src/vendor.ts +1 -0
  65. package/testing/fake-claude.d.ts +5 -0
  66. package/testing/fake-claude.d.ts.map +1 -1
  67. package/testing/fake-claude.js +13 -3
  68. package/testing/fake-elements.d.ts +5 -8
  69. package/testing/fake-elements.d.ts.map +1 -1
  70. package/testing/fake-elements.js +6 -6
  71. package/testing/fake-files.js +5 -5
  72. package/testing.d.ts +4 -2
  73. package/testing.d.ts.map +1 -1
  74. package/testing.js +65 -9
  75. package/ui/define-pane.d.ts +5 -1
  76. package/ui/define-pane.d.ts.map +1 -1
  77. package/utils/call-effects.d.ts.map +1 -1
  78. package/utils/call-effects.js +12 -11
  79. package/utils/parse-shell.d.ts +5 -9
  80. package/utils/parse-shell.d.ts.map +1 -1
  81. package/utils/parse-shell.js +5 -10
  82. package/utils/paths.d.ts +0 -3
  83. package/utils/paths.d.ts.map +1 -1
  84. package/utils/paths.js +3 -18
  85. package/vendor.d.ts +1 -0
  86. package/vendor.d.ts.map +1 -1
  87. package/vendor.js +209 -12
@@ -1,9 +1,10 @@
1
1
  import { elements } from './fake-elements.js';
2
2
  export function fakeClaude(plugin) {
3
3
  const calls = [];
4
- const fakes = { process: {}, fs: {}, http: {}, settings: {}, ui: {}, agent: {}, clock: {}, cmod: {} };
4
+ const fakes = { process: {}, fs: {}, http: {}, settings: {}, ui: {}, session: {}, agent: {}, clock: {}, cmod: {} };
5
5
  const shown = { toasts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [] };
6
6
  const unplacedPanes = new Set();
7
+ let focusedPane;
7
8
  const store = new Map();
8
9
  const faked = (call, answer) => {
9
10
  return (...args) => {
@@ -41,6 +42,8 @@ export function fakeClaude(plugin) {
41
42
  if (answer.isPlaced) {
42
43
  shown.openPanes.add(pane.id);
43
44
  unplacedPanes.delete(pane.id);
45
+ if (pane.focus === true)
46
+ focusedPane = pane.id;
44
47
  }
45
48
  else if (!shown.openPanes.has(pane.id)) {
46
49
  unplacedPanes.add(pane.id);
@@ -51,11 +54,14 @@ export function fakeClaude(plugin) {
51
54
  calls.push({ call: 'ui.close', args: [pane] });
52
55
  shown.openPanes.delete(pane.id);
53
56
  unplacedPanes.delete(pane.id);
57
+ if (focusedPane === pane.id)
58
+ focusedPane = undefined;
54
59
  },
55
60
  panes: async () => [
56
- ...[...shown.openPanes].map((id) => ({ id, title: id, isShown: true, isFocused: false, isPlaced: true })),
61
+ ...[...shown.openPanes].map((id) => ({ id, title: id, isShown: true, isFocused: id === focusedPane, isPlaced: true })),
57
62
  ...[...unplacedPanes].map((id) => ({ id, title: id, isShown: false, isFocused: false, isPlaced: false })),
58
63
  ],
64
+ scroll: rejected('ui.scroll', () => fakes.ui.scroll ?? (async () => ({}))),
59
65
  resolve: () => elements,
60
66
  },
61
67
  process: {
@@ -71,7 +77,10 @@ export function fakeClaude(plugin) {
71
77
  },
72
78
  http: { fetch: rejected('http.fetch', () => fakes.http.fetch) },
73
79
  settings: { read: rejected('settings.read', () => fakes.settings.read) },
74
- agent: { list: rejected('agent.list', () => fakes.agent.list) },
80
+ agent: {
81
+ list: rejected('agent.list', () => fakes.agent.list),
82
+ spawn: rejected('agent.spawn', () => fakes.agent.spawn),
83
+ },
75
84
  store: {
76
85
  get: async (key) => store.get(key),
77
86
  set: async (key, value) => {
@@ -94,6 +103,7 @@ export function fakeClaude(plugin) {
94
103
  model: async () => 'test-model',
95
104
  usage: async () => ({ context: { window: 200000 }, rateLimits: {}, cost: { usd: 0 } }),
96
105
  surfaces: async () => ['terminal'],
106
+ messages: rejected('session.messages', () => fakes.session.messages),
97
107
  },
98
108
  command: {
99
109
  register: async (command) => {
@@ -1,14 +1,11 @@
1
1
  import type { ElementTable, RenderNode } from 'claude-code';
2
- type Button = {
3
- readonly type: 'Button';
4
- readonly props: {
5
- readonly key: string;
6
- readonly label: string;
7
- readonly onPress: () => unknown;
8
- };
2
+ type Drawn = {
3
+ readonly type: string;
4
+ readonly props: Record<string, unknown>;
5
+ readonly children?: readonly RenderNode[];
9
6
  };
10
7
  export declare const elements: ElementTable;
11
- export declare function findButton(node: RenderNode, key: string): Button | undefined;
8
+ export declare function findElement(node: RenderNode, type: string, key: string): Drawn['props'] | undefined;
12
9
  export declare function rowsOf(node: RenderNode, width: number): string[];
13
10
  export declare function textOf(node: RenderNode): string;
14
11
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"fake-elements.d.ts","sourceRoot":"","sources":["../src/testing/fake-elements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAiB,UAAU,EAAE,MAAM,aAAa,CAAA;AAI1E,KAAK,MAAM,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAA;KAAE,CAAA;CAAE,CAAA;AAiB5I,eAAO,MAAM,QAAQ,EAUL,YAAY,CAAA;AAE5B,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAS5E;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAMhE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAiB/C"}
1
+ {"version":3,"file":"fake-elements.d.ts","sourceRoot":"","sources":["../src/testing/fake-elements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAiB,UAAU,EAAE,MAAM,aAAa,CAAA;AAE1E,KAAK,KAAK,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,UAAU,EAAE,CAAA;CAAE,CAAA;AAiB1H,eAAO,MAAM,QAAQ,EAUL,YAAY,CAAA;AAE5B,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,SAAS,CASnG;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAMhE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAiB/C"}
@@ -23,14 +23,14 @@ export const elements = {
23
23
  Select: element('Select'),
24
24
  Image: element('Image'),
25
25
  };
26
- export function findButton(node, key) {
26
+ export function findElement(node, type, key) {
27
27
  if (typeof node === 'string')
28
28
  return undefined;
29
- const { type, props, children = [] } = insideOf(node);
30
- if (type === 'Button' && (props['key'] === key || props['label'] === key))
31
- return node;
32
- for (const child of children) {
33
- const found = findButton(child, key);
29
+ const drawn = insideOf(node);
30
+ if (drawn.type === type && (drawn.props['key'] === key || (type === 'Button' && drawn.props['label'] === key)))
31
+ return drawn.props;
32
+ for (const child of drawn.children ?? []) {
33
+ const found = findElement(child, type, key);
34
34
  if (found !== undefined)
35
35
  return found;
36
36
  }
@@ -1,8 +1,8 @@
1
- import { resolvePath } from '../utils/paths.js';
1
+ import { resolve } from '../vendor.js';
2
2
  export function fakeFiles(files, links = {}) {
3
- const contents = new Map(Object.entries(files).map(([path, text]) => [resolvePath(path, '/'), text]));
3
+ const contents = new Map(Object.entries(files).map(([path, text]) => [resolve('/', path), text]));
4
4
  const realPathOf = (path) => {
5
- const absolute = resolvePath(path, '/');
5
+ const absolute = resolve('/', path);
6
6
  for (const [link, target] of Object.entries(links)) {
7
7
  if (absolute === link || absolute.startsWith(`${link}/`))
8
8
  return realPathOf(`${target}${absolute.slice(link.length)}`);
@@ -37,7 +37,7 @@ export function fakeFiles(files, links = {}) {
37
37
  throw new Error(`ENOENT: no such directory, list '${path}'`);
38
38
  const names = new Set(below(folder).map((file) => file.split('/')[0]));
39
39
  return [...names].map((name) => {
40
- const entry = resolvePath(name, folder);
40
+ const entry = resolve(folder, name);
41
41
  if (Object.hasOwn(links, entry))
42
42
  return { name, kind: 'other', size: 0, mtimeMs: 0, isLink: true };
43
43
  return { name, kind: contents.has(entry) ? 'file' : 'dir', size: sizeOf(entry), mtimeMs: 0, isLink: false };
@@ -47,7 +47,7 @@ export function fakeFiles(files, links = {}) {
47
47
  stat: async (path, options) => {
48
48
  const realPath = realPathOf(path);
49
49
  const kind = kindOf(realPath);
50
- const isLink = Object.hasOwn(links, resolvePath(path, '/'));
50
+ const isLink = Object.hasOwn(links, resolve('/', path));
51
51
  if (kind === undefined && isLink)
52
52
  return { kind: 'other', size: 0, mtimeMs: 0, isLink };
53
53
  if (kind === undefined)
package/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Args, ClassicHookInputs, CmodDependencies, EventResult, RenderComponent } from 'claude-code';
1
+ import type { Args, ClassicHookInputs, CmodDependencies, EventResult, RenderComponent, UiInputArgument } from 'claude-code';
2
2
  import type { Reply } from './jobs/slash-command.js';
3
3
  import type { ModDefinition, ModEvent } from './mod.js';
4
4
  import type { RoutedEvent } from './runtime/hooks.js';
@@ -30,7 +30,9 @@ export type TestedMod<State extends object> = {
30
30
  }>>(slot: S, props: Omit<SlotProps<S>, 'Default'>, requestId?: string): Promise<string[]>;
31
31
  type(line: string): Promise<Reply>;
32
32
  callTool(name: string, input: Record<string, unknown>): Promise<EventResult<'tool.call'>>;
33
- press(paneId: string, key: string): Promise<void>;
33
+ press(paneId: string, key: string, link?: string): Promise<void>;
34
+ input(paneId: string, key: string, text: string, kind?: UiInputArgument['kind']): Promise<void>;
35
+ select(paneId: string, key: string, value: string): Promise<void>;
34
36
  moveTo(projectRoot: string, cwd?: string): Promise<void>;
35
37
  readonly state: Readonly<State>;
36
38
  readonly calls: readonly TestCall[];
package/testing.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,gBAAgB,EAAyC,WAAW,EAAwC,eAAe,EAAgC,MAAM,aAAa,CAAA;AACrN,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAIvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAErD,OAAO,EAAc,KAAK,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAG5F,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAEpD,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAEtE,MAAM,MAAM,WAAW,CAAC,KAAK,SAAS,MAAM,IAAI;IAC9C,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,EAAE,QAAQ,IAAI,MAAM,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;KAAE,CAAA;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,YAAY,CAAC,EAAE;QAAE,QAAQ,EAAE,IAAI,IAAI,MAAM,gBAAgB,CAAC,CAAC,EAAE,gBAAgB,CAAC,IAAI,CAAC;KAAE,CAAA;IAC9F,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAClD,CAAA;AAED,KAAK,WAAW,GAAG,YAAY,GAAG,iBAAiB,GAAG,KAAK,GAAG,iBAAiB,GAAG,aAAa,CAAA;AAE/F,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,QAAQ,IAAI,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAEnK,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,IAAI;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,IAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAA;IAClI,IAAI,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;IACtG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACvB,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IACxC,KAAK,CAAC,CAAC,SAAS,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAA;KAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IACpK,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAA;IACzF,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxD,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAA;IACnC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CACtB,CAAA;AAUD,wBAAgB,OAAO,CAAC,KAAK,SAAS,MAAM,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,OAAO,GAAE,WAAW,CAAC,KAAK,CAAM,GAAG,SAAS,CAAC,KAAK,CAAC,CAmIlI"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["src/testing.ts"],"names":[],"mappings":"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"}
package/testing.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { storeFolder } from './records.js';
2
2
  import { answerCall, notInstalled } from './runtime/dependencies.js';
3
+ import { toolInputOf } from './runtime/tool-calls.js';
3
4
  import { createLifecycle } from './runtime/lifecycle.js';
4
5
  import { fakeClaude } from './testing/fake-claude.js';
5
- import { elements, findButton, rowsOf } from './testing/fake-elements.js';
6
+ import { elements, findElement, rowsOf } from './testing/fake-elements.js';
6
7
  import { fakeFiles } from './testing/fake-files.js';
7
8
  const defaultColumns = 80;
8
9
  const defaultRows = 24;
@@ -24,6 +25,13 @@ export function testMod(definition, options = {}) {
24
25
  fake.claude.session.root = async () => projectRoot;
25
26
  fake.claude.session.cwd = async () => cwd;
26
27
  fake.fakes.cmod.call = fakeDependencies(options.dependencies ?? {});
28
+ const firedAgents = new Map();
29
+ const listAgents = fake.claude.agent.list;
30
+ fake.claude.agent.list = async () => {
31
+ if (fake.fakes.agent.list !== undefined || firedAgents.size === 0)
32
+ return listAgents();
33
+ return [...firedAgents].map(([id, type]) => ({ id, type, description: '', status: 'running' }));
34
+ };
27
35
  Object.assign(fake.fakes.fs, fakeFiles(options.files ?? {}));
28
36
  const starting = options.state;
29
37
  const declared = Object.entries(definition.state ?? {});
@@ -55,10 +63,18 @@ export function testMod(definition, options = {}) {
55
63
  }
56
64
  const opened = fake.calls.findLast((call) => call.call === 'ui.open' && call.args[0].id === paneId)?.args[0];
57
65
  const columns = opened?.columns ?? defaultColumns;
58
- const props = { title: opened?.title ?? paneId, isFocused: false, bodyColumns: columns, placement: 'dock', scroll: { offset: 0, bodyRows: defaultRows }, view: {} };
66
+ const isFocused = (await fake.claude.ui.panes()).some((open) => open.id === paneId && open.isFocused);
67
+ const props = { title: opened?.title ?? paneId, isFocused, bodyColumns: columns, placement: 'dock', scroll: { offset: 0, bodyRows: defaultRows }, view: {} };
59
68
  const drawing = await route('ui.render', { surface: 'terminal', component: 'Pane', requestId: paneId, props }, elements.Box({}));
60
69
  return { drawing, columns };
61
70
  };
71
+ const drawnElement = async (paneId, type, key) => {
72
+ const props = findElement((await drawPane(paneId)).drawing, type, key);
73
+ if (props === undefined)
74
+ throw new Error(`The pane "${paneId}" of ${name} draws no ${type} with the ${type === 'Button' ? 'key or label' : 'key'} "${key}".`);
75
+ return props;
76
+ };
77
+ const eventIn = (paneId, key) => ({ plugin: name, element: key, component: 'Pane', requestId: paneId, surface: 'terminal' });
62
78
  const drawSlot = async (component, props, requestId = `${component}_${(slotDraws += 1)}`) => {
63
79
  await start();
64
80
  const input = { surface: 'terminal', component, requestId, props };
@@ -78,9 +94,29 @@ export function testMod(definition, options = {}) {
78
94
  const filled = { session_id: sessionId, transcript_path: '/test/transcript.jsonl', cwd, hook_event_name: event, ...toolUse, ...input };
79
95
  if (event !== 'PreToolUse')
80
96
  return route(`classic.${event}`, filled, below);
81
- const { tool_name, tool_input, tool_use_id } = filled;
82
- return route('classic.PreToolUse', { ...tool_input, tool: tool_name, tool_use_id }, below);
97
+ return firePreToolUse(filled, below);
83
98
  });
99
+ const firePreToolUse = async ({ tool_name, tool_input, tool_use_id, agent_id, agent_type }, below) => {
100
+ await start();
101
+ if (agent_id !== undefined && agent_type !== undefined)
102
+ firedAgents.set(agent_id, agent_type);
103
+ const envelope = { ...tool_input, tool: tool_name, tool_use_id, ...(agent_id === undefined ? {} : { agentId: agent_id }) };
104
+ const asks = below?.deny === undefined ? { decision: 'ask' } : { decision: 'deny', reason: below.deny };
105
+ let reached = envelope;
106
+ let check = asks;
107
+ const answer = await lifecycle.route('tool.call', envelope, async (e) => {
108
+ reached = e;
109
+ check = await lifecycle.route('tool.check', { tool: e.tool, input: toolInputOf(e), tool_use_id: e.tool_use_id, ...(e.agentId === undefined ? {} : { agentId: e.agentId }) }, async () => asks);
110
+ return (check.decision === 'deny' ? { deny: check.reason ?? '' } : { result: '' });
111
+ });
112
+ const updatedInput = toolInputOf(reached);
113
+ const decided = check === asks ? {} : check.decision === 'allow' ? { allow: true } : check.decision === 'ask' ? { ask: check.reason ?? '' } : {};
114
+ return {
115
+ ...(answer.deny === undefined ? decided : { deny: answer.deny }),
116
+ ...(JSON.stringify(updatedInput) === JSON.stringify(tool_input) ? {} : { updatedInput }),
117
+ ...(answer.context === undefined || answer.context.length === 0 ? {} : { additionalContext: [...answer.context] }),
118
+ };
119
+ };
84
120
  return {
85
121
  start,
86
122
  fire,
@@ -114,11 +150,31 @@ export function testMod(definition, options = {}) {
114
150
  throw new Error(`${name} has no tool ${toolName}. Add it in setup with mod.use(tool({ name: '${toolName}', … })).`);
115
151
  return answer;
116
152
  },
117
- async press(paneId, key) {
118
- const button = findButton((await drawPane(paneId)).drawing, key);
119
- if (button === undefined)
120
- throw new Error(`The pane "${paneId}" of ${name} draws no Button with the key or label "${key}".`);
121
- await button.props.onPress();
153
+ async press(paneId, key, link) {
154
+ const props = await drawnElement(paneId, link === undefined ? 'Button' : 'Markdown', key);
155
+ if (link !== undefined && props['onLinkPress'] === undefined)
156
+ throw new Error(`The Markdown "${key}" in the pane "${paneId}" of ${name} has no onLinkPress, so Claude Code opens its links itself.`);
157
+ const pressed = { ...eventIn(paneId, String(props['key'])), ...(link === undefined ? {} : { link: { href: link } }) };
158
+ await lifecycle.route('ui.press', pressed, async (e) => {
159
+ if (e.link === undefined)
160
+ await props['onPress'](e);
161
+ else
162
+ await props['onLinkPress'](e.link, e);
163
+ return { element: e.element };
164
+ });
165
+ },
166
+ async input(paneId, key, text, kind = 'submit') {
167
+ const props = await drawnElement(paneId, 'Input', key);
168
+ const typed = { ...eventIn(paneId, key), kind, value: text };
169
+ await props[kind === 'submit' ? 'onSubmit' : 'onInput']?.(text, typed);
170
+ },
171
+ async select(paneId, key, value) {
172
+ const props = await drawnElement(paneId, 'Select', key);
173
+ const options = props['options'].map((option) => option.value);
174
+ if (!options.includes(value))
175
+ throw new Error(`The Select "${key}" in the pane "${paneId}" of ${name} has no option "${value}". Its options are ${options.map((option) => `"${option}"`).join(', ')}.`);
176
+ const picked = { ...eventIn(paneId, key), value };
177
+ await props['onSelect'](value, picked);
122
178
  },
123
179
  async moveTo(nextRoot, nextCwd = nextRoot) {
124
180
  await start();
@@ -1,11 +1,15 @@
1
- import type { Frozen, RenderElement, RenderPropsOf } from 'claude-code';
1
+ import type { Frozen, PaneCloseInput, RenderElement, RenderPropsOf, UiScrollInput } from 'claude-code';
2
2
  import type { Mod } from '../mod.js';
3
3
  export type Pane<State extends object = Record<never, never>> = {
4
4
  readonly id: string;
5
5
  readonly title: string;
6
6
  readonly columns?: number | ((state: State) => number | undefined);
7
7
  readonly rows?: number | ((state: State) => number | undefined);
8
+ readonly closeOnEscape?: true;
9
+ readonly holdToasts?: true;
8
10
  render(mod: Mod<State>, props: Frozen<RenderPropsOf['Pane']>): RenderElement;
11
+ onScroll?(mod: Mod<State>, e: Frozen<UiScrollInput>): void | Promise<void>;
12
+ onClose?(mod: Mod<State>, e: Frozen<PaneCloseInput>): void | Promise<void>;
9
13
  };
10
14
  export declare function definePane<State extends object = Record<never, never>>(pane: Pane<State>): Pane<State>;
11
15
  //# sourceMappingURL=define-pane.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"define-pane.d.ts","sourceRoot":"","sources":["../src/ui/define-pane.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AACvE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAEpC,MAAM,MAAM,IAAI,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI;IAC9D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,GAAG,SAAS,CAAC,CAAA;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,GAAG,SAAS,CAAC,CAAA;IAC/D,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,aAAa,CAAA;CAC7E,CAAA;AAMD,wBAAgB,UAAU,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAOtG"}
1
+ {"version":3,"file":"define-pane.d.ts","sourceRoot":"","sources":["../src/ui/define-pane.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AACtG,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAEpC,MAAM,MAAM,IAAI,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI;IAC9D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,GAAG,SAAS,CAAC,CAAA;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,GAAG,SAAS,CAAC,CAAA;IAC/D,QAAQ,CAAC,aAAa,CAAC,EAAE,IAAI,CAAA;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,CAAA;IAC1B,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,aAAa,CAAA;IAC5E,QAAQ,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,aAAa,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1E,OAAO,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3E,CAAA;AAMD,wBAAgB,UAAU,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAOtG"}
@@ -1 +1 @@
1
- {"version":3,"file":"call-effects.d.ts","sourceRoot":"","sources":["../src/utils/call-effects.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACxE,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAErE,MAAM,MAAM,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5F,MAAM,MAAM,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAC7E,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG;IAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE,CAAA;AAClG,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAC9F,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AAClD,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEpE,MAAM,MAAM,QAAQ,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AACrE,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,SAAS,CAAA;CAAE,CAAA;AAC1F,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,YAAY,EAAE,CAAC;QAAC,aAAa,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAA;IACrF,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,MAAM,EAAE,UAAU,EAAE,CAAA;IACpB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B,CAAA;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,UAAU,CAAA;CAAE,GAAG,WAAW,CAsC/G;AAED,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,OAAO,GAAG,QAAQ,CAE1E"}
1
+ {"version":3,"file":"call-effects.d.ts","sourceRoot":"","sources":["../src/utils/call-effects.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAChE,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAGxD,MAAM,MAAM,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5F,MAAM,MAAM,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAC7E,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG;IAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE,CAAA;AAClG,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAC9F,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AAClD,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEpE,MAAM,MAAM,QAAQ,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AACrE,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,SAAS,CAAA;CAAE,CAAA;AAC1F,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,YAAY,EAAE,CAAC;QAAC,aAAa,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAA;IACrF,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,MAAM,EAAE,UAAU,EAAE,CAAA;IACpB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B,CAAA;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,UAAU,CAAA;CAAE,GAAG,WAAW,CAsC/G;AAED,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,OAAO,GAAG,QAAQ,CAE1E"}
@@ -1,14 +1,15 @@
1
- import { parseShellCommands } from './parse-shell.js';
2
- import { expandHome, resolvePath } from './paths.js';
1
+ import { parseShell } from './parse-shell.js';
2
+ import { expandHome } from './paths.js';
3
+ import { resolve } from '../vendor.js';
3
4
  export function callEffects(use, workspace) {
4
5
  const { fs } = workspace;
5
- const resolve = (path, folder = workspace.cwd) => resolvePath(expandHome(path, workspace.home), folder);
6
- const access = (path) => ({ path: resolve(path), contents: undefined });
6
+ const absolute = (path, folder = workspace.cwd) => resolve(folder, expandHome(path, workspace.home));
7
+ const access = (path) => ({ path: absolute(path), contents: undefined });
7
8
  const effects = { shell: undefined, reads: [], writes: [], urls: [], subagent: undefined };
8
9
  if (use.tool === 'Bash') {
9
10
  const line = textField(use, 'command');
10
- const parsed = parseShellCommands(line);
11
- const commands = parsed.commands.map(({ argv, folder }) => ({ argv, folder: resolve(folder) }));
11
+ const parsed = parseShell(line);
12
+ const commands = parsed.commands.map(({ argv, folder }) => ({ argv, folder: absolute(folder) }));
12
13
  effects.shell = { line, commands, isFullyParsed: parsed.isFullyParsed };
13
14
  effects.reads = parsed.reads.map(access);
14
15
  effects.writes = parsed.writes.map(access);
@@ -18,16 +19,16 @@ export function callEffects(use, workspace) {
18
19
  effects.shell = { line: textField(use, 'command'), commands: [], isFullyParsed: false };
19
20
  }
20
21
  else if (use.tool === 'Edit') {
21
- const path = resolve(textField(use, 'file_path'));
22
+ const path = absolute(textField(use, 'file_path'));
22
23
  effects.writes = [{ path, contents: once(() => editedContents(use, path, fs)) }];
23
24
  }
24
25
  else if (use.tool === 'Write') {
25
- const path = resolve(textField(use, 'file_path'));
26
+ const path = absolute(textField(use, 'file_path'));
26
27
  const content = textField(use, 'content');
27
28
  effects.writes = [{ path, contents: once(async () => contentsOf(content, await previousContentOf(path, fs))) }];
28
29
  }
29
30
  else if (use.tool === 'NotebookEdit') {
30
- const path = resolve(textField(use, 'notebook_path'));
31
+ const path = absolute(textField(use, 'notebook_path'));
31
32
  effects.writes = [{ path, contents: once(async () => contentsOf(undefined, await previousContentOf(path, fs))) }];
32
33
  }
33
34
  else if (use.tool === 'Read') {
@@ -37,8 +38,8 @@ export function callEffects(use, workspace) {
37
38
  effects.reads = [access(optionalTextField(use, 'path') ?? '')];
38
39
  }
39
40
  else if (use.tool === 'Glob') {
40
- const folder = resolve(optionalTextField(use, 'path') ?? '');
41
- effects.reads = [{ path: resolve(textField(use, 'pattern'), folder), contents: undefined }];
41
+ const folder = absolute(optionalTextField(use, 'path') ?? '');
42
+ effects.reads = [{ path: absolute(textField(use, 'pattern'), folder), contents: undefined }];
42
43
  }
43
44
  else if (use.tool === 'WebFetch') {
44
45
  effects.urls = [textField(use, 'url')];
@@ -1,18 +1,14 @@
1
- export type ParsedShell = {
2
- commands: [string, ...string[]][];
3
- writes: string[];
4
- reads: string[];
5
- fetches: string[];
6
- isFullyParsed: boolean;
7
- };
8
1
  export type ShellCommand = {
9
2
  argv: [string, ...string[]];
10
3
  folder: string;
11
4
  };
12
- export type ParsedShellCommands = Omit<ParsedShell, 'commands'> & {
5
+ export type ParsedShell = {
13
6
  commands: ShellCommand[];
7
+ writes: string[];
8
+ reads: string[];
9
+ fetches: string[];
10
+ isFullyParsed: boolean;
14
11
  };
15
12
  export declare const dynamicPattern: RegExp;
16
13
  export declare function parseShell(line: string): ParsedShell;
17
- export declare function parseShellCommands(line: string): ParsedShellCommands;
18
14
  //# sourceMappingURL=parse-shell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parse-shell.d.ts","sourceRoot":"","sources":["../src/utils/parse-shell.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,EAAE,CAAA;IACjC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,aAAa,EAAE,OAAO,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1E,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,GAAG;IAAE,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAAA;AA2B9F,eAAO,MAAM,cAAc,QAAS,CAAA;AAiKpC,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAGpD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAIpE"}
1
+ {"version":3,"file":"parse-shell.d.ts","sourceRoot":"","sources":["../src/utils/parse-shell.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1E,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,aAAa,EAAE,OAAO,CAAA;CACvB,CAAA;AA2BD,eAAO,MAAM,cAAc,QAAS,CAAA;AAiKpC,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAIpD"}
@@ -1,5 +1,4 @@
1
- import { nameOf } from './paths.js';
2
- import { parse } from '../vendor.js';
1
+ import { basename, parse } from '../vendor.js';
3
2
  const markerPattern = /\u0001(\d+)\u0001/g;
4
3
  const descriptorPattern = /[0-9]+(?=[<>])/y;
5
4
  const assignmentPattern = /^[A-Za-z_][A-Za-z0-9_]*(\[[^\]]*\])?\+?=/;
@@ -163,10 +162,6 @@ const inlineCodeFlags = new Map([
163
162
  ['osascript', ['-e']],
164
163
  ]);
165
164
  export function parseShell(line) {
166
- const parsed = parseShellCommands(line);
167
- return { ...parsed, commands: parsed.commands.map((command) => command.argv) };
168
- }
169
- export function parseShellCommands(line) {
170
165
  const result = { commands: [], writes: [], reads: [], fetches: [], isFullyParsed: true };
171
166
  parseLine(line, '', result);
172
167
  return result;
@@ -533,7 +528,7 @@ function wordOf(token, markers, folder, result) {
533
528
  return {
534
529
  text: raw.replace(markerPattern, (_, index) => markers[Number(index)].raw),
535
530
  isGlob: typeof token !== 'string',
536
- isDynamic: /[$\u0001]/.test(nameOf(raw)),
531
+ isDynamic: /[$\u0001]/.test(basename(raw)),
537
532
  };
538
533
  }
539
534
  function finish(command, operator, folder, result) {
@@ -573,7 +568,7 @@ function addRedirect({ operator, target }, folder, result) {
573
568
  function addCommand(first, args, folder, stdin, result) {
574
569
  if (first.isDynamic || first.isGlob)
575
570
  result.isFullyParsed = false;
576
- const program = nameOf(first.text);
571
+ const program = basename(first.text);
577
572
  if (program === 'git')
578
573
  addGit(first, args, folder, result);
579
574
  else if (program === 'find')
@@ -734,7 +729,7 @@ function addCopy(program, args, folder, result) {
734
729
  const target = options.findLast((option) => program.targetOptions?.includes(option.name) === true)?.value;
735
730
  const only = operands[0];
736
731
  if (program.linksHere === true && target === undefined && operands.length === 1 && only !== undefined) {
737
- addPath(result, 'writes', { ...only, text: nameOf(only.text) }, folder);
732
+ addPath(result, 'writes', { ...only, text: basename(only.text) }, folder);
738
733
  return;
739
734
  }
740
735
  const destination = target ?? operands.at(-1);
@@ -746,7 +741,7 @@ function addCopy(program, args, folder, result) {
746
741
  if (program.sources !== undefined)
747
742
  addPath(result, program.sources, source, folder);
748
743
  if (isLocal(destination) && !source.isGlob && !destination.isGlob)
749
- addPathText(result, 'writes', `${destination.text.replace(/\/+$/, '')}/${nameOf(source.text)}`, folder);
744
+ addPathText(result, 'writes', `${destination.text.replace(/\/+$/, '')}/${basename(source.text)}`, folder);
750
745
  }
751
746
  if (isLocal(destination))
752
747
  addPath(result, 'writes', destination, folder);
package/utils/paths.d.ts CHANGED
@@ -7,9 +7,6 @@ export type FileSystem = {
7
7
  exists: (path: string) => Promise<boolean>;
8
8
  };
9
9
  export declare function expandHome(path: string, home: string): string;
10
- export declare function resolvePath(path: string, folder: string): string;
11
- export declare function parentOf(path: string): string;
12
- export declare function nameOf(path: string): string;
13
10
  export declare function relativePath(base: string, path: string): string | undefined;
14
11
  export declare function realPathOf(path: string, fs: FileSystem): Promise<string | undefined>;
15
12
  //# sourceMappingURL=paths.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/utils/paths.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACvC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtF,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC3C,CAAA;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAOhE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI3E;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAK1F"}
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/utils/paths.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACvC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtF,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC3C,CAAA;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI3E;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAK1F"}
package/utils/paths.js CHANGED
@@ -1,23 +1,8 @@
1
+ import { basename, dirname, resolve } from '../vendor.js';
1
2
  export function expandHome(path, home) {
2
3
  const prefix = /^(~|\$HOME)(?=\/|$)/.exec(path)?.[0];
3
4
  return prefix === undefined ? path : `${home}${path.slice(prefix.length)}`;
4
5
  }
5
- export function resolvePath(path, folder) {
6
- const parts = [];
7
- for (const part of (path.startsWith('/') ? path : `${folder}/${path}`).split('/')) {
8
- if (part === '..')
9
- parts.pop();
10
- else if (part !== '' && part !== '.')
11
- parts.push(part);
12
- }
13
- return `/${parts.join('/')}`;
14
- }
15
- export function parentOf(path) {
16
- return resolvePath('..', path);
17
- }
18
- export function nameOf(path) {
19
- return path.slice(path.lastIndexOf('/') + 1);
20
- }
21
6
  export function relativePath(base, path) {
22
7
  if (path === base)
23
8
  return '';
@@ -28,6 +13,6 @@ export async function realPathOf(path, fs) {
28
13
  const stat = await fs.stat(path).catch(() => undefined);
29
14
  if (stat !== undefined || path === '/')
30
15
  return stat?.realPath;
31
- const folder = await realPathOf(parentOf(path), fs);
32
- return folder === undefined ? undefined : resolvePath(nameOf(path), folder);
16
+ const folder = await realPathOf(dirname(path), fs);
17
+ return folder === undefined ? undefined : resolve(folder, basename(path));
33
18
  }
package/vendor.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Validator } from '@cfworker/json-schema';
2
+ export { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve } from 'pathe';
2
3
  export { default as picomatch } from 'picomatch/posix';
3
4
  export { parse, quote, type GlobPattern, type ParseEntry } from 'shell-quote';
4
5
  //# sourceMappingURL=vendor.d.ts.map
package/vendor.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"vendor.d.ts","sourceRoot":"","sources":["src/vendor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,iBAAiB,CAAA;AACtD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"vendor.d.ts","sourceRoot":"","sources":["src/vendor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAClG,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,iBAAiB,CAAA;AACtD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA"}