@kosuke-ai/cli 0.0.56 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/README.md +31 -448
  2. package/dist/commands/auth.js +117 -0
  3. package/dist/generated/commands/deploy/cancel.js +17 -0
  4. package/dist/generated/commands/deploy/messages.js +17 -0
  5. package/dist/generated/commands/deploy/run.js +60 -0
  6. package/dist/generated/commands/index.js +132 -0
  7. package/dist/generated/commands/preview/get.js +25 -0
  8. package/dist/generated/commands/preview/health.js +25 -0
  9. package/dist/generated/commands/preview/reset.js +27 -0
  10. package/dist/generated/commands/preview/share-create.js +36 -0
  11. package/dist/generated/commands/preview/share-delete.js +26 -0
  12. package/dist/generated/commands/preview/share-get.js +26 -0
  13. package/dist/generated/commands/preview/shared.js +17 -0
  14. package/dist/generated/commands/projects/create.js +37 -0
  15. package/dist/generated/commands/projects/delete.js +17 -0
  16. package/dist/generated/commands/projects/env-vars-add.js +21 -0
  17. package/dist/generated/commands/projects/env-vars-remove.js +21 -0
  18. package/dist/generated/commands/projects/get.js +17 -0
  19. package/dist/generated/commands/projects/list.js +15 -0
  20. package/dist/generated/commands/projects/services-add.js +87 -0
  21. package/dist/generated/commands/projects/services-list.js +18 -0
  22. package/dist/generated/commands/projects/services-remove.js +19 -0
  23. package/dist/generated/commands/projects/services-update.js +81 -0
  24. package/dist/generated/commands/projects/settings-get.js +18 -0
  25. package/dist/generated/commands/projects/settings-import.js +91 -0
  26. package/dist/generated/commands/projects/settings-update.js +77 -0
  27. package/dist/generated/commands/projects/skills.js +35 -0
  28. package/dist/generated/commands/projects/update.js +27 -0
  29. package/dist/generated/commands/sessions/activate.js +18 -0
  30. package/dist/generated/commands/sessions/checklist-generate.js +19 -0
  31. package/dist/generated/commands/sessions/checklist-get.js +19 -0
  32. package/dist/generated/commands/sessions/comments-add.js +63 -0
  33. package/dist/generated/commands/sessions/comments-delete.js +20 -0
  34. package/dist/generated/commands/sessions/comments-list.js +19 -0
  35. package/dist/generated/commands/sessions/comments-update.js +23 -0
  36. package/dist/generated/commands/sessions/create.js +126 -0
  37. package/dist/generated/commands/sessions/delete.js +18 -0
  38. package/dist/generated/commands/sessions/diff-files.js +19 -0
  39. package/dist/generated/commands/sessions/diff-patch.js +86 -0
  40. package/dist/generated/commands/sessions/edit-prompt.js +59 -0
  41. package/dist/generated/commands/sessions/files-get.js +27 -0
  42. package/dist/generated/commands/sessions/files-list.js +19 -0
  43. package/dist/generated/commands/sessions/files-write.js +41 -0
  44. package/dist/generated/commands/sessions/get.js +18 -0
  45. package/dist/generated/commands/sessions/git-commit-message.js +19 -0
  46. package/dist/generated/commands/sessions/git-commit.js +29 -0
  47. package/dist/generated/commands/sessions/git-push.js +19 -0
  48. package/dist/generated/commands/sessions/git-state.js +19 -0
  49. package/dist/generated/commands/sessions/jobs-cancel.js +20 -0
  50. package/dist/generated/commands/sessions/jobs-get.js +20 -0
  51. package/dist/generated/commands/sessions/list.js +81 -0
  52. package/dist/generated/commands/sessions/messages.js +18 -0
  53. package/dist/generated/commands/sessions/search.js +23 -0
  54. package/dist/generated/commands/sessions/send.js +98 -0
  55. package/dist/generated/commands/sessions/update.js +72 -0
  56. package/dist/generated/commands/workspaces/access-status.js +29 -0
  57. package/dist/generated/commands/workspaces/ai-provider-get.js +18 -0
  58. package/dist/generated/commands/workspaces/ai-provider-remove.js +42 -0
  59. package/dist/generated/commands/workspaces/ai-provider-set.js +21 -0
  60. package/dist/generated/commands/workspaces/credits.js +17 -0
  61. package/dist/generated/commands/workspaces/get.js +17 -0
  62. package/dist/generated/commands/workspaces/getting-started-get.js +18 -0
  63. package/dist/generated/commands/workspaces/getting-started-update.js +32 -0
  64. package/dist/generated/commands/workspaces/list.js +15 -0
  65. package/dist/generated/commands/workspaces/members.js +25 -0
  66. package/dist/generated/commands/workspaces/sandbox-limit.js +17 -0
  67. package/dist/generated/commands/workspaces/stop-sandboxes.js +27 -0
  68. package/dist/generated/commands/workspaces/usage.js +32 -0
  69. package/dist/index.js +5 -516
  70. package/dist/program.js +46 -0
  71. package/dist/runtime/attach.js +139 -0
  72. package/dist/runtime/attachments.js +58 -0
  73. package/dist/runtime/client.js +12 -0
  74. package/dist/runtime/config.js +49 -0
  75. package/dist/runtime/credentials.js +24 -0
  76. package/dist/runtime/descriptor.js +7 -0
  77. package/dist/runtime/invoke.js +152 -0
  78. package/dist/runtime/runtime.js +41 -0
  79. package/dist/runtime/version.js +5 -0
  80. package/openapi.json +5222 -0
  81. package/package.json +21 -79
  82. package/LICENSE +0 -21
  83. package/claude.costs.json +0 -34
  84. package/dist/__tests__/utils/file-discovery.test.d.ts +0 -5
  85. package/dist/__tests__/utils/file-discovery.test.d.ts.map +0 -1
  86. package/dist/__tests__/utils/file-discovery.test.js +0 -51
  87. package/dist/__tests__/utils/file-discovery.test.js.map +0 -1
  88. package/dist/__tests__/utils/logger-nologs.test.d.ts +0 -5
  89. package/dist/__tests__/utils/logger-nologs.test.d.ts.map +0 -1
  90. package/dist/__tests__/utils/logger-nologs.test.js +0 -34
  91. package/dist/__tests__/utils/logger-nologs.test.js.map +0 -1
  92. package/dist/__tests__/utils/logger.test.d.ts +0 -5
  93. package/dist/__tests__/utils/logger.test.d.ts.map +0 -1
  94. package/dist/__tests__/utils/logger.test.js +0 -243
  95. package/dist/__tests__/utils/logger.test.js.map +0 -1
  96. package/dist/__tests__/utils/validator.test.d.ts +0 -5
  97. package/dist/__tests__/utils/validator.test.d.ts.map +0 -1
  98. package/dist/__tests__/utils/validator.test.js +0 -206
  99. package/dist/__tests__/utils/validator.test.js.map +0 -1
  100. package/dist/index.d.ts +0 -35
  101. package/dist/index.d.ts.map +0 -1
  102. package/dist/index.js.map +0 -1
  103. package/dist/kosuke/commands/analyse.d.ts +0 -15
  104. package/dist/kosuke/commands/analyse.d.ts.map +0 -1
  105. package/dist/kosuke/commands/analyse.js +0 -273
  106. package/dist/kosuke/commands/analyse.js.map +0 -1
  107. package/dist/kosuke/commands/build.d.ts +0 -35
  108. package/dist/kosuke/commands/build.d.ts.map +0 -1
  109. package/dist/kosuke/commands/build.js +0 -659
  110. package/dist/kosuke/commands/build.js.map +0 -1
  111. package/dist/kosuke/commands/getcode.d.ts +0 -29
  112. package/dist/kosuke/commands/getcode.d.ts.map +0 -1
  113. package/dist/kosuke/commands/getcode.js +0 -221
  114. package/dist/kosuke/commands/getcode.js.map +0 -1
  115. package/dist/kosuke/commands/lint.d.ts +0 -21
  116. package/dist/kosuke/commands/lint.d.ts.map +0 -1
  117. package/dist/kosuke/commands/lint.js +0 -321
  118. package/dist/kosuke/commands/lint.js.map +0 -1
  119. package/dist/kosuke/commands/migrate.d.ts +0 -38
  120. package/dist/kosuke/commands/migrate.d.ts.map +0 -1
  121. package/dist/kosuke/commands/migrate.js +0 -284
  122. package/dist/kosuke/commands/migrate.js.map +0 -1
  123. package/dist/kosuke/commands/plan.d.ts +0 -28
  124. package/dist/kosuke/commands/plan.d.ts.map +0 -1
  125. package/dist/kosuke/commands/plan.js +0 -630
  126. package/dist/kosuke/commands/plan.js.map +0 -1
  127. package/dist/kosuke/commands/requirements.d.ts +0 -52
  128. package/dist/kosuke/commands/requirements.d.ts.map +0 -1
  129. package/dist/kosuke/commands/requirements.js +0 -615
  130. package/dist/kosuke/commands/requirements.js.map +0 -1
  131. package/dist/kosuke/commands/review.d.ts +0 -26
  132. package/dist/kosuke/commands/review.d.ts.map +0 -1
  133. package/dist/kosuke/commands/review.js +0 -303
  134. package/dist/kosuke/commands/review.js.map +0 -1
  135. package/dist/kosuke/commands/serve.d.ts +0 -9
  136. package/dist/kosuke/commands/serve.d.ts.map +0 -1
  137. package/dist/kosuke/commands/serve.js +0 -17
  138. package/dist/kosuke/commands/serve.js.map +0 -1
  139. package/dist/kosuke/commands/ship.d.ts +0 -37
  140. package/dist/kosuke/commands/ship.d.ts.map +0 -1
  141. package/dist/kosuke/commands/ship.js +0 -375
  142. package/dist/kosuke/commands/ship.js.map +0 -1
  143. package/dist/kosuke/commands/sync-rules.d.ts +0 -9
  144. package/dist/kosuke/commands/sync-rules.d.ts.map +0 -1
  145. package/dist/kosuke/commands/sync-rules.js +0 -379
  146. package/dist/kosuke/commands/sync-rules.js.map +0 -1
  147. package/dist/kosuke/commands/test.d.ts +0 -42
  148. package/dist/kosuke/commands/test.d.ts.map +0 -1
  149. package/dist/kosuke/commands/test.js +0 -540
  150. package/dist/kosuke/commands/test.js.map +0 -1
  151. package/dist/kosuke/commands/tickets.d.ts +0 -43
  152. package/dist/kosuke/commands/tickets.d.ts.map +0 -1
  153. package/dist/kosuke/commands/tickets.js +0 -499
  154. package/dist/kosuke/commands/tickets.js.map +0 -1
  155. package/dist/kosuke/serve/middleware/error-handler.d.ts +0 -14
  156. package/dist/kosuke/serve/middleware/error-handler.d.ts.map +0 -1
  157. package/dist/kosuke/serve/middleware/error-handler.js +0 -42
  158. package/dist/kosuke/serve/middleware/error-handler.js.map +0 -1
  159. package/dist/kosuke/serve/middleware/validation.d.ts +0 -13
  160. package/dist/kosuke/serve/middleware/validation.d.ts.map +0 -1
  161. package/dist/kosuke/serve/middleware/validation.js +0 -38
  162. package/dist/kosuke/serve/middleware/validation.js.map +0 -1
  163. package/dist/kosuke/serve/routes/build.d.ts +0 -6
  164. package/dist/kosuke/serve/routes/build.d.ts.map +0 -1
  165. package/dist/kosuke/serve/routes/build.js +0 -226
  166. package/dist/kosuke/serve/routes/build.js.map +0 -1
  167. package/dist/kosuke/serve/routes/health.d.ts +0 -6
  168. package/dist/kosuke/serve/routes/health.d.ts.map +0 -1
  169. package/dist/kosuke/serve/routes/health.js +0 -11
  170. package/dist/kosuke/serve/routes/health.js.map +0 -1
  171. package/dist/kosuke/serve/routes/plan.d.ts +0 -6
  172. package/dist/kosuke/serve/routes/plan.d.ts.map +0 -1
  173. package/dist/kosuke/serve/routes/plan.js +0 -106
  174. package/dist/kosuke/serve/routes/plan.js.map +0 -1
  175. package/dist/kosuke/serve/server.d.ts +0 -16
  176. package/dist/kosuke/serve/server.d.ts.map +0 -1
  177. package/dist/kosuke/serve/server.js +0 -67
  178. package/dist/kosuke/serve/server.js.map +0 -1
  179. package/dist/kosuke/serve/validation/build.d.ts +0 -41
  180. package/dist/kosuke/serve/validation/build.d.ts.map +0 -1
  181. package/dist/kosuke/serve/validation/build.js +0 -20
  182. package/dist/kosuke/serve/validation/build.js.map +0 -1
  183. package/dist/kosuke/serve/validation/plan.d.ts +0 -26
  184. package/dist/kosuke/serve/validation/plan.d.ts.map +0 -1
  185. package/dist/kosuke/serve/validation/plan.js +0 -15
  186. package/dist/kosuke/serve/validation/plan.js.map +0 -1
  187. package/dist/kosuke/types.d.ts +0 -469
  188. package/dist/kosuke/types.d.ts.map +0 -1
  189. package/dist/kosuke/types.js +0 -5
  190. package/dist/kosuke/types.js.map +0 -1
  191. package/dist/kosuke/utils/batch-creator.d.ts +0 -14
  192. package/dist/kosuke/utils/batch-creator.d.ts.map +0 -1
  193. package/dist/kosuke/utils/batch-creator.js +0 -76
  194. package/dist/kosuke/utils/batch-creator.js.map +0 -1
  195. package/dist/kosuke/utils/claude-agent.d.ts +0 -145
  196. package/dist/kosuke/utils/claude-agent.d.ts.map +0 -1
  197. package/dist/kosuke/utils/claude-agent.js +0 -504
  198. package/dist/kosuke/utils/claude-agent.js.map +0 -1
  199. package/dist/kosuke/utils/constants.d.ts +0 -40
  200. package/dist/kosuke/utils/constants.d.ts.map +0 -1
  201. package/dist/kosuke/utils/constants.js +0 -54
  202. package/dist/kosuke/utils/constants.js.map +0 -1
  203. package/dist/kosuke/utils/file-discovery.d.ts +0 -15
  204. package/dist/kosuke/utils/file-discovery.d.ts.map +0 -1
  205. package/dist/kosuke/utils/file-discovery.js +0 -80
  206. package/dist/kosuke/utils/file-discovery.js.map +0 -1
  207. package/dist/kosuke/utils/git.d.ts +0 -41
  208. package/dist/kosuke/utils/git.d.ts.map +0 -1
  209. package/dist/kosuke/utils/git.js +0 -145
  210. package/dist/kosuke/utils/git.js.map +0 -1
  211. package/dist/kosuke/utils/github.d.ts +0 -17
  212. package/dist/kosuke/utils/github.d.ts.map +0 -1
  213. package/dist/kosuke/utils/github.js +0 -22
  214. package/dist/kosuke/utils/github.js.map +0 -1
  215. package/dist/kosuke/utils/interactive-input.d.ts +0 -22
  216. package/dist/kosuke/utils/interactive-input.d.ts.map +0 -1
  217. package/dist/kosuke/utils/interactive-input.js +0 -124
  218. package/dist/kosuke/utils/interactive-input.js.map +0 -1
  219. package/dist/kosuke/utils/logger.d.ts +0 -161
  220. package/dist/kosuke/utils/logger.d.ts.map +0 -1
  221. package/dist/kosuke/utils/logger.js +0 -234
  222. package/dist/kosuke/utils/logger.js.map +0 -1
  223. package/dist/kosuke/utils/playwright-mcp-client.d.ts +0 -46
  224. package/dist/kosuke/utils/playwright-mcp-client.d.ts.map +0 -1
  225. package/dist/kosuke/utils/playwright-mcp-client.js +0 -135
  226. package/dist/kosuke/utils/playwright-mcp-client.js.map +0 -1
  227. package/dist/kosuke/utils/pr-orchestrator.d.ts +0 -49
  228. package/dist/kosuke/utils/pr-orchestrator.d.ts.map +0 -1
  229. package/dist/kosuke/utils/pr-orchestrator.js +0 -154
  230. package/dist/kosuke/utils/pr-orchestrator.js.map +0 -1
  231. package/dist/kosuke/utils/repository-manager.d.ts +0 -13
  232. package/dist/kosuke/utils/repository-manager.d.ts.map +0 -1
  233. package/dist/kosuke/utils/repository-manager.js +0 -94
  234. package/dist/kosuke/utils/repository-manager.js.map +0 -1
  235. package/dist/kosuke/utils/repository-resolver.d.ts +0 -12
  236. package/dist/kosuke/utils/repository-resolver.d.ts.map +0 -1
  237. package/dist/kosuke/utils/repository-resolver.js +0 -186
  238. package/dist/kosuke/utils/repository-resolver.js.map +0 -1
  239. package/dist/kosuke/utils/tickets-manager.d.ts +0 -64
  240. package/dist/kosuke/utils/tickets-manager.d.ts.map +0 -1
  241. package/dist/kosuke/utils/tickets-manager.js +0 -331
  242. package/dist/kosuke/utils/tickets-manager.js.map +0 -1
  243. package/dist/kosuke/utils/validator.d.ts +0 -45
  244. package/dist/kosuke/utils/validator.d.ts.map +0 -1
  245. package/dist/kosuke/utils/validator.js +0 -248
  246. package/dist/kosuke/utils/validator.js.map +0 -1
  247. package/dist/lib.d.ts +0 -96
  248. package/dist/lib.d.ts.map +0 -1
  249. package/dist/lib.js +0 -95
  250. package/dist/lib.js.map +0 -1
  251. package/dist/package.json +0 -99
@@ -0,0 +1,139 @@
1
+ import { Argument, Command, InvalidArgumentError, Option } from 'commander';
2
+ import { buildRequest, execute, readWholeBody, UsageError } from './invoke.js';
3
+ /**
4
+ * One commander command per operation descriptor: positionals for path
5
+ * params, a flag per top-level field, `--body`/`--body-file` for a whole
6
+ * body and `--attach` where the operation takes files. The command's help is
7
+ * the contract's summary, verbatim.
8
+ */
9
+ export function attachOperation(root, operation, runtime) {
10
+ const parent = groupFor(root, operation.command.slice(0, -1));
11
+ const leaf = parent.command(operation.command[operation.command.length - 1]);
12
+ leaf.description(operation.summary);
13
+ for (const name of operation.pathParams) {
14
+ leaf.addArgument(new Argument(`<${name}>`, humanize(name)));
15
+ }
16
+ const fields = dedupe([
17
+ ...operation.query,
18
+ ...(operation.body?.fields ?? []),
19
+ ...(operation.multipart ?? []),
20
+ ]);
21
+ for (const field of fields) {
22
+ leaf.addOption(optionFor(field));
23
+ // Both spellings, so a field whose meaningful value is false (members
24
+ // --no-include-invitations) can be said; neither given leaves it unsent.
25
+ if (field.kind === 'boolean') {
26
+ leaf.addOption(new Option(`--no-${field.flag}`, `Send ${field.name} as false.`));
27
+ }
28
+ }
29
+ const takesBody = operation.body !== null || operation.multipart !== null;
30
+ if (takesBody) {
31
+ leaf.option('--body <json>', 'The whole request body as JSON; overrides field flags.');
32
+ leaf.option('--body-file <path>', 'Read the whole request body from a file, or - for stdin.');
33
+ }
34
+ if (operation.attachments) {
35
+ leaf.option('--attach <path>', 'Attach a file; repeat for several. Sends the request as multipart.', collect, []);
36
+ }
37
+ leaf.action(async (...args) => {
38
+ const options = args[args.length - 2];
39
+ const positionals = args.slice(0, operation.pathParams.length);
40
+ const input = {
41
+ pathParams: Object.fromEntries(operation.pathParams.map((name, i) => [name, positionals[i]])),
42
+ fields: Object.fromEntries(fields.map((field) => [field.name, options[optionKey(field.flag)]])),
43
+ wholeBody: readWholeBody({
44
+ body: options.body,
45
+ bodyFile: options.bodyFile,
46
+ }),
47
+ attachments: options.attach ?? [],
48
+ };
49
+ let request;
50
+ try {
51
+ request = buildRequest(operation, input);
52
+ }
53
+ catch (error) {
54
+ if (error instanceof UsageError)
55
+ leaf.error(error.message);
56
+ throw error;
57
+ }
58
+ const client = runtime.client({ anonymous: operation.anonymous });
59
+ const outcome = await execute(client, request);
60
+ runtime.output(outcome);
61
+ });
62
+ return leaf;
63
+ }
64
+ function groupFor(root, path) {
65
+ let current = root;
66
+ for (const segment of path) {
67
+ const existing = current.commands.find((command) => command.name() === segment);
68
+ current = existing ?? current.command(segment).description(`${humanize(segment)} commands`);
69
+ }
70
+ return current;
71
+ }
72
+ function optionFor(field) {
73
+ const description = field.description ?? '';
74
+ if (field.kind === 'boolean')
75
+ return new Option(`--${field.flag}`, description);
76
+ const option = new Option(`--${field.flag} <value>`, description);
77
+ if (field.choices)
78
+ option.choices([...field.choices]);
79
+ if (field.kind === 'number' || field.kind === 'integer')
80
+ option.argParser(numberParser(field));
81
+ if (field.kind === 'json')
82
+ option.argParser(jsonParser);
83
+ return option;
84
+ }
85
+ /**
86
+ * Decimal digits only. `Number()` reads '' as 0, '0x10' as 16 and '1e999' as
87
+ * Infinity (which JSON writes as null), none of which a customer meant.
88
+ */
89
+ const DECIMAL = /^-?\d+(\.\d+)?$/;
90
+ const INTEGER = /^-?\d+$/;
91
+ function numberParser(field) {
92
+ return (raw) => {
93
+ const pattern = field.kind === 'integer' ? INTEGER : DECIMAL;
94
+ if (!pattern.test(raw)) {
95
+ throw new InvalidArgumentError(field.kind === 'integer' ? 'Expected an integer.' : 'Expected a number.');
96
+ }
97
+ const value = Number(raw);
98
+ if (!Number.isFinite(value))
99
+ throw new InvalidArgumentError('Expected a finite number.');
100
+ return value;
101
+ };
102
+ }
103
+ function jsonParser(raw) {
104
+ try {
105
+ return JSON.parse(raw);
106
+ }
107
+ catch {
108
+ throw new InvalidArgumentError('Expected JSON.');
109
+ }
110
+ }
111
+ function collect(value, previous) {
112
+ return [...previous, value];
113
+ }
114
+ /**
115
+ * One option per flag. The generator gives the same wire name on the JSON and
116
+ * the form branch one flag, and two wire names never one flag; this keys on
117
+ * the flag because that is what commander refuses to register twice.
118
+ */
119
+ function dedupe(fields) {
120
+ const seen = new Set();
121
+ return fields.filter((field) => {
122
+ if (seen.has(field.flag))
123
+ return false;
124
+ seen.add(field.flag);
125
+ return true;
126
+ });
127
+ }
128
+ /** commander stores `--exclude-archived` as `excludeArchived`. */
129
+ export function optionKey(flag) {
130
+ return flag.replace(/-([a-z0-9])/g, (_, letter) => letter.toUpperCase());
131
+ }
132
+ /** `projectId` reads as `Project id`; `env-vars` as `Env vars`. */
133
+ export function humanize(name) {
134
+ const words = name
135
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
136
+ .replace(/-/g, ' ')
137
+ .toLowerCase();
138
+ return words.charAt(0).toUpperCase() + words.slice(1);
139
+ }
@@ -0,0 +1,58 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { basename, extname } from 'node:path';
3
+ /**
4
+ * Files on a multipart request, the way the platform reads them.
5
+ *
6
+ * Hand-written on purpose. The API takes `attachmentCount` and one file per
7
+ * `attachment_<i>` field, and a field whose name carries an index is not
8
+ * something an OpenAPI document can describe: the spec marks the operation
9
+ * with `x-kosuke-attachments` and stops there, so the convention lives here,
10
+ * outside the generated tree, in the one place that has to know it.
11
+ *
12
+ * Limits mirror the server's (10 files, 10 MiB each) so a bad batch fails
13
+ * before any byte is uploaded rather than after.
14
+ */
15
+ const MAX_ATTACHMENT_FILES = 10;
16
+ const MAX_ATTACHMENT_FILE_SIZE = 10 * 1024 * 1024;
17
+ /**
18
+ * The server trusts an explicit media type and only infers one, from the
19
+ * extension, for documents sent as octet-stream. Images are never inferred,
20
+ * so a screenshot has to arrive typed or it is refused.
21
+ */
22
+ const MEDIA_TYPES = {
23
+ '.jpg': 'image/jpeg',
24
+ '.jpeg': 'image/jpeg',
25
+ '.png': 'image/png',
26
+ '.gif': 'image/gif',
27
+ '.webp': 'image/webp',
28
+ '.pdf': 'application/pdf',
29
+ '.txt': 'text/plain',
30
+ '.md': 'text/markdown',
31
+ '.csv': 'text/csv',
32
+ '.tsv': 'text/tab-separated-values',
33
+ '.json': 'application/json',
34
+ '.xml': 'application/xml',
35
+ '.yaml': 'application/x-yaml',
36
+ '.yml': 'application/x-yaml',
37
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
38
+ '.xls': 'application/vnd.ms-excel',
39
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
40
+ };
41
+ function mediaTypeFor(path) {
42
+ return MEDIA_TYPES[extname(path).toLowerCase()] ?? 'application/octet-stream';
43
+ }
44
+ export function appendAttachments(form, paths) {
45
+ if (paths.length > MAX_ATTACHMENT_FILES) {
46
+ throw new Error(`At most ${MAX_ATTACHMENT_FILES} attachments per request; ${paths.length} given.`);
47
+ }
48
+ // Size from the inode before the read: a file past the cap must fail with
49
+ // this message, not with the runtime's own error once it has been loaded.
50
+ for (const path of paths) {
51
+ if (statSync(path).size > MAX_ATTACHMENT_FILE_SIZE) {
52
+ throw new Error(`${path} is larger than the 10 MiB attachment limit.`);
53
+ }
54
+ }
55
+ const files = paths.map((path) => new File([readFileSync(path)], basename(path), { type: mediaTypeFor(path) }));
56
+ form.set('attachmentCount', String(files.length));
57
+ files.forEach((file, index) => form.set(`attachment_${index}`, file));
58
+ }
@@ -0,0 +1,12 @@
1
+ import createClient, {} from 'openapi-fetch';
2
+ import { version } from './version.js';
3
+ export function createApiClient({ apiUrl, apiKey, fetch }) {
4
+ return createClient({
5
+ baseUrl: apiUrl,
6
+ headers: {
7
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
8
+ 'User-Agent': `@kosuke-ai/cli/${version}`,
9
+ },
10
+ ...(fetch ? { fetch } : {}),
11
+ });
12
+ }
@@ -0,0 +1,49 @@
1
+ import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ export const DEFAULT_API_URL = 'https://app.kosuke.ai';
5
+ function configDir(env = process.env) {
6
+ if (env.KOSUKE_CONFIG_DIR)
7
+ return env.KOSUKE_CONFIG_DIR;
8
+ if (env.XDG_CONFIG_HOME)
9
+ return join(env.XDG_CONFIG_HOME, 'kosuke');
10
+ if (process.platform === 'win32' && env.APPDATA)
11
+ return join(env.APPDATA, 'kosuke');
12
+ return join(homedir(), '.config', 'kosuke');
13
+ }
14
+ export function credentialsPath(env = process.env) {
15
+ return join(configDir(env), 'credentials.json');
16
+ }
17
+ export function readCredentials(env = process.env) {
18
+ try {
19
+ const parsed = JSON.parse(readFileSync(credentialsPath(env), 'utf8'));
20
+ if (parsed && typeof parsed === 'object' && 'apiKey' in parsed) {
21
+ const { apiKey, apiUrl } = parsed;
22
+ if (typeof apiKey === 'string' && apiKey.length > 0) {
23
+ return { apiKey, ...(typeof apiUrl === 'string' ? { apiUrl } : {}) };
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export function writeCredentials(credentials, env = process.env) {
33
+ const path = credentialsPath(env);
34
+ mkdirSync(configDir(env), { recursive: true, mode: 0o700 });
35
+ writeFileSync(path, JSON.stringify(credentials, null, 2) + '\n', { mode: 0o600 });
36
+ // `mode` on write only applies to a file being created; an existing one keeps
37
+ // whatever it had, and a token file must not.
38
+ chmodSync(path, 0o600);
39
+ return path;
40
+ }
41
+ export function deleteCredentials(env = process.env) {
42
+ try {
43
+ rmSync(credentialsPath(env));
44
+ return true;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
@@ -0,0 +1,24 @@
1
+ import { DEFAULT_API_URL, readCredentials } from './config.js';
2
+ export function resolveCredentials(flags, env = process.env) {
3
+ const stored = readCredentials(env);
4
+ const apiKey = flags.apiKey
5
+ ? { value: flags.apiKey, source: 'flag' }
6
+ : env.KOSUKE_API_KEY
7
+ ? { value: env.KOSUKE_API_KEY, source: 'env' }
8
+ : stored
9
+ ? { value: stored.apiKey, source: 'file' }
10
+ : null;
11
+ const apiUrl = flags.apiUrl
12
+ ? { value: flags.apiUrl, source: 'flag' }
13
+ : env.KOSUKE_API_URL
14
+ ? { value: env.KOSUKE_API_URL, source: 'env' }
15
+ : stored?.apiUrl
16
+ ? { value: stored.apiUrl, source: 'file' }
17
+ : { value: DEFAULT_API_URL, source: 'default' };
18
+ return {
19
+ apiKey: apiKey?.value ?? null,
20
+ apiKeySource: apiKey?.source ?? null,
21
+ apiUrl: apiUrl.value.replace(/\/+$/, ''),
22
+ apiUrlSource: apiUrl.source,
23
+ };
24
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * What the generator writes per operation, and what the runtime builds a
3
+ * command from. Data only: the generated tree holds no logic, so it stays
4
+ * small, byte-stable and reviewable, and every behaviour lives in one
5
+ * hand-written place.
6
+ */
7
+ export {};
@@ -0,0 +1,152 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { appendAttachments } from './attachments.js';
3
+ export class UsageError extends Error {
4
+ }
5
+ export function buildRequest(operation, input) {
6
+ const queryNames = new Set(operation.query.map((field) => field.name));
7
+ const query = {};
8
+ for (const field of operation.query) {
9
+ const value = input.fields[field.name];
10
+ if (value !== undefined)
11
+ query[field.name] = value;
12
+ }
13
+ for (const field of operation.query) {
14
+ if (field.required && query[field.name] === undefined) {
15
+ throw new UsageError(`--${field.flag} is required.`);
16
+ }
17
+ }
18
+ const bodyFields = Object.fromEntries(Object.entries(input.fields).filter(([name, value]) => !queryNames.has(name) && value !== undefined));
19
+ return {
20
+ method: operation.method,
21
+ path: operation.path,
22
+ pathParams: input.pathParams,
23
+ query,
24
+ body: buildBody(operation, bodyFields, input),
25
+ };
26
+ }
27
+ /**
28
+ * Which media type to send. Multipart when the operation only takes that, when
29
+ * files are attached, or when a field given exists only on the form branch;
30
+ * JSON otherwise, including an empty `{}` for a body-taking operation given
31
+ * nothing, which is how the deploy route reads "start a deploy".
32
+ */
33
+ function buildBody(operation, fields, input) {
34
+ const takesJson = operation.body !== null;
35
+ const takesMultipart = operation.multipart !== null;
36
+ if (!takesJson && !takesMultipart) {
37
+ if (input.wholeBody !== undefined || Object.keys(fields).length || input.attachments.length) {
38
+ throw new UsageError(`kosuke ${operation.command.join(' ')} takes no body.`);
39
+ }
40
+ return null;
41
+ }
42
+ if (input.attachments.length && !operation.attachments) {
43
+ throw new UsageError(`kosuke ${operation.command.join(' ')} takes no attachments.`);
44
+ }
45
+ // The keys that decide the media type come from --body as much as from the
46
+ // flags: `--body '{"content":"hi"}'` on the deploy route names a form-only
47
+ // field, and sending it as JSON would have zod strip it and the handler read
48
+ // an empty body, which there means "start a fresh deploy".
49
+ const jsonFields = operation.body?.fields ?? null;
50
+ const jsonNames = new Set(jsonFields?.map((field) => field.name) ?? []);
51
+ // A field flag always names a field of one branch, so it decides on its
52
+ // own. The keys of --body decide only against a flat JSON body: a non-flat
53
+ // one (an array, a union) has no field names to hold them against, and
54
+ // sending such a --body as the form would be a guess. No operation has that
55
+ // shape beside a form branch today; if one does, --body stays JSON there.
56
+ const given = [
57
+ ...Object.keys(fields),
58
+ ...(jsonFields ? Object.keys(asRecordOrEmpty(input.wholeBody)) : []),
59
+ ];
60
+ const multipartOnly = given.some((name) => !jsonNames.has(name));
61
+ const useMultipart = takesMultipart && (!takesJson || input.attachments.length > 0 || multipartOnly);
62
+ if (useMultipart) {
63
+ const form = new FormData();
64
+ const source = input.wholeBody !== undefined ? asRecord(input.wholeBody, 'The body') : fields;
65
+ const known = new Map((operation.multipart ?? []).map((field) => [field.name, field]));
66
+ for (const [name, value] of Object.entries(source)) {
67
+ if (!known.has(name))
68
+ throw new UsageError(`Unknown form field "${name}".`);
69
+ if (value === undefined || value === null)
70
+ continue;
71
+ form.set(name, formValue(value));
72
+ }
73
+ for (const field of operation.multipart ?? []) {
74
+ if (field.required && !form.has(field.name)) {
75
+ throw new UsageError(`--${field.flag} is required.`);
76
+ }
77
+ }
78
+ if (input.attachments.length)
79
+ appendAttachments(form, input.attachments);
80
+ return { kind: 'multipart', value: form };
81
+ }
82
+ if (input.wholeBody !== undefined)
83
+ return { kind: 'json', value: input.wholeBody };
84
+ if (operation.body?.fields === null) {
85
+ if (Object.keys(fields).length) {
86
+ throw new UsageError(`This body is not a flat object; pass it whole with --body, --body-file or on stdin.`);
87
+ }
88
+ if (operation.body.required) {
89
+ throw new UsageError(`A body is required: pass --body, or --body-file with a path or - for stdin.`);
90
+ }
91
+ return null;
92
+ }
93
+ for (const field of operation.body?.fields ?? []) {
94
+ if (field.required && fields[field.name] === undefined) {
95
+ throw new UsageError(`--${field.flag} is required.`);
96
+ }
97
+ }
98
+ return { kind: 'json', value: fields };
99
+ }
100
+ /**
101
+ * A form field is a string. A value already parsed as JSON (an object from a
102
+ * `json` flag, or from --body) goes back to its text, which is what the
103
+ * handler JSON-parses on the form branch; scalars go as they print.
104
+ */
105
+ function formValue(value) {
106
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
107
+ }
108
+ function asRecordOrEmpty(value) {
109
+ return value && typeof value === 'object' && !Array.isArray(value)
110
+ ? value
111
+ : {};
112
+ }
113
+ function asRecord(value, what) {
114
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
115
+ return value;
116
+ }
117
+ throw new UsageError(`${what} must be a JSON object for a multipart request.`);
118
+ }
119
+ /**
120
+ * `--body '<json>'`, or `--body-file <path>` with `-` for stdin. Stdin is read
121
+ * only when asked for: a CLI that reads an inherited pipe on its own hangs
122
+ * whenever the parent never closes it, which is a bug that only shows up in
123
+ * someone else's script.
124
+ */
125
+ export function readWholeBody(options, readStdin = () => readFileSync(0, 'utf8')) {
126
+ let text = null;
127
+ if (options.body !== undefined)
128
+ text = options.body;
129
+ else if (options.bodyFile === '-')
130
+ text = readStdin();
131
+ else if (options.bodyFile !== undefined)
132
+ text = readFileSync(options.bodyFile, 'utf8');
133
+ if (text === null || text.trim() === '')
134
+ return undefined;
135
+ try {
136
+ return JSON.parse(text);
137
+ }
138
+ catch {
139
+ throw new UsageError('The body is not valid JSON.');
140
+ }
141
+ }
142
+ export async function execute(client, request) {
143
+ const init = {
144
+ params: { path: request.pathParams, query: request.query },
145
+ };
146
+ if (request.body)
147
+ init.body = request.body.value;
148
+ // `request()` rather than the typed verbs: the descriptor decides the path at
149
+ // run time, and openapi-fetch's per-path types cannot follow a variable.
150
+ const { data, error, response } = await client.request(request.method, request.path, init);
151
+ return { ok: response.ok, status: response.status, body: response.ok ? data : error };
152
+ }
@@ -0,0 +1,41 @@
1
+ import { createApiClient } from './client.js';
2
+ import { resolveCredentials, } from './credentials.js';
3
+ export function createRuntime({ flags, env = process.env, fetch, stdout = (line) => process.stdout.write(line + '\n'), stderr = (line) => process.stderr.write(line + '\n'), exit = (code) => {
4
+ process.exitCode = code;
5
+ }, }) {
6
+ let resolved = null;
7
+ const credentials = () => (resolved ??= resolveCredentials(flags, env));
8
+ return {
9
+ credentials,
10
+ client({ anonymous = false } = {}) {
11
+ const { apiKey, apiUrl } = credentials();
12
+ if (!apiKey && !anonymous) {
13
+ stderr('No auth token. Run `kosuke auth login`, set KOSUKE_API_KEY, or pass --api-key. ' +
14
+ 'Create one in Settings on the platform.');
15
+ exit(1);
16
+ throw new ExitSignal();
17
+ }
18
+ return createApiClient({ apiUrl, apiKey: anonymous ? null : apiKey, fetch });
19
+ },
20
+ output(outcome) {
21
+ if (outcome.ok) {
22
+ // 204 has nothing to print; anything else is passed through as JSON so
23
+ // `| jq` works without the platform CLI knowing the shape.
24
+ if (outcome.body !== undefined)
25
+ stdout(render(outcome.body));
26
+ return;
27
+ }
28
+ stderr(render(outcome.body ?? { error: `HTTP ${outcome.status}` }));
29
+ exit(1);
30
+ },
31
+ };
32
+ }
33
+ function render(body) {
34
+ return typeof body === 'string' ? body : JSON.stringify(body, null, 2);
35
+ }
36
+ /** Thrown after `exit` so the action stops without printing a second error. */
37
+ export class ExitSignal extends Error {
38
+ constructor() {
39
+ super('exit');
40
+ }
41
+ }
@@ -0,0 +1,5 @@
1
+ import { createRequire } from 'node:module';
2
+ // Read at run time rather than imported with an attribute: the package's own
3
+ // manifest ships with it, and this works the same from src/ and from dist/.
4
+ const manifest = createRequire(import.meta.url)('../../package.json');
5
+ export const version = manifest.version;