@otto-code/cli 0.8.10 → 0.8.13

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/dist/cli.js +1 -5
  2. package/dist/commands/daemon/index.js +1 -0
  3. package/dist/commands/daemon/local-daemon.js +3 -0
  4. package/dist/commands/daemon/pair.d.ts +27 -1
  5. package/dist/commands/daemon/pair.js +133 -46
  6. package/dist/commands/daemon/start.js +1 -0
  7. package/dist/commands/daemon/status.d.ts +11 -0
  8. package/dist/commands/daemon/status.js +47 -22
  9. package/dist/commands/hub/authority.d.ts +17 -0
  10. package/dist/commands/hub/authority.js +18 -0
  11. package/dist/commands/hub/connect.d.ts +21 -0
  12. package/dist/commands/hub/connect.js +34 -0
  13. package/dist/commands/hub/credentials.d.ts +21 -0
  14. package/dist/commands/hub/credentials.js +143 -0
  15. package/dist/commands/hub/daemon-client.d.ts +27 -0
  16. package/dist/commands/hub/daemon-client.js +14 -0
  17. package/dist/commands/hub/deploy-bundle.d.ts +16 -0
  18. package/dist/commands/hub/deploy-bundle.js +248 -0
  19. package/dist/commands/hub/deploy.d.ts +40 -0
  20. package/dist/commands/hub/deploy.js +90 -0
  21. package/dist/commands/hub/disconnect.d.ts +16 -0
  22. package/dist/commands/hub/disconnect.js +24 -0
  23. package/dist/commands/hub/error.d.ts +6 -0
  24. package/dist/commands/hub/error.js +9 -0
  25. package/dist/commands/hub/help.d.ts +3 -0
  26. package/dist/commands/hub/help.js +5 -0
  27. package/dist/commands/hub/hub-client/index.d.ts +19 -0
  28. package/dist/commands/hub/hub-client/index.js +99 -0
  29. package/dist/commands/hub/hub-client/internal/contracts.d.ts +86 -0
  30. package/dist/commands/hub/hub-client/internal/contracts.js +62 -0
  31. package/dist/commands/hub/hub-client/internal/problem.d.ts +3 -0
  32. package/dist/commands/hub/hub-client/internal/problem.js +85 -0
  33. package/dist/commands/hub/hub-client/internal/transport.d.ts +15 -0
  34. package/dist/commands/hub/hub-client/internal/transport.js +30 -0
  35. package/dist/commands/hub/index.d.ts +15 -25
  36. package/dist/commands/hub/index.js +67 -73
  37. package/dist/commands/hub/init-plan.d.ts +50 -0
  38. package/dist/commands/hub/init-plan.js +136 -0
  39. package/dist/commands/hub/init.d.ts +20 -0
  40. package/dist/commands/hub/init.js +415 -0
  41. package/dist/commands/hub/{device-authorization.d.ts → login-flow.d.ts} +12 -15
  42. package/dist/commands/hub/login-flow.js +78 -0
  43. package/dist/commands/hub/login.d.ts +21 -0
  44. package/dist/commands/hub/login.js +34 -0
  45. package/dist/commands/hub/logout.d.ts +32 -0
  46. package/dist/commands/hub/logout.js +71 -0
  47. package/dist/commands/hub/origin.d.ts +2 -0
  48. package/dist/commands/hub/origin.js +27 -0
  49. package/dist/commands/hub/projects.d.ts +24 -0
  50. package/dist/commands/hub/projects.js +49 -0
  51. package/dist/commands/hub/reporter.d.ts +10 -0
  52. package/dist/commands/hub/reporter.js +11 -0
  53. package/dist/commands/hub/status-output.d.ts +13 -0
  54. package/dist/commands/hub/status-output.js +30 -0
  55. package/dist/commands/onboard.js +20 -40
  56. package/dist/commands/provider/diagnostic.d.ts +12 -0
  57. package/dist/commands/provider/diagnostic.js +31 -0
  58. package/dist/commands/provider/index.js +5 -0
  59. package/dist/commands/schedule/types.d.ts +1 -4
  60. package/dist/commands/workspace/index.js +10 -0
  61. package/dist/commands/workspace/rename.d.ts +22 -0
  62. package/dist/commands/workspace/rename.js +61 -0
  63. package/dist/output/types.d.ts +1 -1
  64. package/dist/run.js +8 -1
  65. package/dist/utils/client.js +4 -1
  66. package/package.json +5 -5
  67. package/dist/commands/chat/create.d.ts +0 -9
  68. package/dist/commands/chat/create.js +0 -23
  69. package/dist/commands/chat/delete.d.ts +0 -6
  70. package/dist/commands/chat/delete.js +0 -20
  71. package/dist/commands/chat/index.d.ts +0 -3
  72. package/dist/commands/chat/index.js +0 -47
  73. package/dist/commands/chat/inspect.d.ts +0 -6
  74. package/dist/commands/chat/inspect.js +0 -20
  75. package/dist/commands/chat/ls.d.ts +0 -6
  76. package/dist/commands/chat/ls.js +0 -20
  77. package/dist/commands/chat/post.d.ts +0 -9
  78. package/dist/commands/chat/post.js +0 -28
  79. package/dist/commands/chat/read.d.ts +0 -11
  80. package/dist/commands/chat/read.js +0 -40
  81. package/dist/commands/chat/schema.d.ts +0 -36
  82. package/dist/commands/chat/schema.js +0 -81
  83. package/dist/commands/chat/shared.d.ts +0 -19
  84. package/dist/commands/chat/shared.js +0 -118
  85. package/dist/commands/chat/wait.d.ts +0 -9
  86. package/dist/commands/chat/wait.js +0 -45
  87. package/dist/commands/hub/cloud-device-authorization.d.ts +0 -45
  88. package/dist/commands/hub/cloud-device-authorization.js +0 -92
  89. package/dist/commands/hub/device-authorization.js +0 -87
  90. package/dist/commands/loop/index.d.ts +0 -3
  91. package/dist/commands/loop/index.js +0 -18
  92. package/dist/commands/loop/inspect.d.ts +0 -13
  93. package/dist/commands/loop/inspect.js +0 -100
  94. package/dist/commands/loop/logs.d.ts +0 -8
  95. package/dist/commands/loop/logs.js +0 -69
  96. package/dist/commands/loop/ls.d.ts +0 -18
  97. package/dist/commands/loop/ls.js +0 -62
  98. package/dist/commands/loop/run.d.ts +0 -30
  99. package/dist/commands/loop/run.js +0 -162
  100. package/dist/commands/loop/stop.d.ts +0 -15
  101. package/dist/commands/loop/stop.js +0 -56
  102. package/dist/commands/loop/types.d.ts +0 -138
  103. package/dist/commands/loop/types.js +0 -2
@@ -0,0 +1,143 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ import { z } from "zod";
6
+ import { HubCommandError } from "./error.js";
7
+ import { normalizeHubOrigin } from "./origin.js";
8
+ const PRIVATE_DIRECTORY_MODE = 0o700;
9
+ const PRIVATE_FILE_MODE = 0o600;
10
+ const credentialRecordSchema = z
11
+ .object({ origin: z.string(), credential: z.string().min(1) })
12
+ .strict();
13
+ const credentialFileSchema = z
14
+ .object({
15
+ version: z.literal(1),
16
+ activeOrigin: z.string().nullable(),
17
+ credentials: z.array(credentialRecordSchema),
18
+ })
19
+ .strict()
20
+ .superRefine((value, context) => {
21
+ const origins = new Set();
22
+ for (const record of value.credentials) {
23
+ let normalized;
24
+ try {
25
+ normalized = normalizeHubOrigin(record.origin);
26
+ }
27
+ catch {
28
+ context.addIssue({ code: "custom", message: "Credential origin is invalid" });
29
+ continue;
30
+ }
31
+ if (normalized !== record.origin || origins.has(record.origin)) {
32
+ context.addIssue({
33
+ code: "custom",
34
+ message: "Credential origins must be unique and normalized",
35
+ });
36
+ }
37
+ origins.add(record.origin);
38
+ }
39
+ if (value.activeOrigin !== null && !origins.has(value.activeOrigin)) {
40
+ context.addIssue({ code: "custom", message: "Active credential is missing" });
41
+ }
42
+ });
43
+ export class PrivateHubCredentialStore {
44
+ constructor(env = process.env) {
45
+ this.filePath = path.join(resolveOttoHome(env), "hub-credentials.json");
46
+ }
47
+ active() {
48
+ const data = this.read();
49
+ if (data.activeOrigin === null)
50
+ return null;
51
+ return data.credentials.find((record) => record.origin === data.activeOrigin) ?? null;
52
+ }
53
+ get(origin) {
54
+ const normalizedOrigin = normalizeHubOrigin(origin);
55
+ return this.read().credentials.find((record) => record.origin === normalizedOrigin) ?? null;
56
+ }
57
+ save(credential) {
58
+ const origin = normalizeHubOrigin(credential.origin);
59
+ const current = this.read();
60
+ const remaining = current.credentials.filter((record) => record.origin !== origin);
61
+ this.write({
62
+ version: 1,
63
+ activeOrigin: origin,
64
+ credentials: [...remaining, { origin, credential: credential.credential }],
65
+ });
66
+ }
67
+ logoutActive() {
68
+ const current = this.read();
69
+ if (current.activeOrigin === null)
70
+ return null;
71
+ const removed = current.credentials.find((record) => record.origin === current.activeOrigin);
72
+ if (removed === undefined)
73
+ throw invalidCredentialFile();
74
+ this.write({
75
+ version: 1,
76
+ activeOrigin: null,
77
+ credentials: current.credentials.filter((record) => record.origin !== current.activeOrigin),
78
+ });
79
+ return removed;
80
+ }
81
+ read() {
82
+ let contents;
83
+ try {
84
+ contents = readFileSync(this.filePath, "utf8");
85
+ }
86
+ catch (error) {
87
+ if (isMissingFile(error))
88
+ return { version: 1, activeOrigin: null, credentials: [] };
89
+ throw credentialStorageError();
90
+ }
91
+ chmodPrivate(this.filePath, PRIVATE_FILE_MODE);
92
+ try {
93
+ return credentialFileSchema.parse(JSON.parse(contents));
94
+ }
95
+ catch {
96
+ throw invalidCredentialFile();
97
+ }
98
+ }
99
+ write(value) {
100
+ const parsed = credentialFileSchema.parse(value);
101
+ const directory = path.dirname(this.filePath);
102
+ mkdirSync(directory, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
103
+ chmodPrivate(directory, PRIVATE_DIRECTORY_MODE);
104
+ const temporaryPath = path.join(directory, `.${path.basename(this.filePath)}.${process.pid}.${randomUUID()}.tmp`);
105
+ try {
106
+ writeFileSync(temporaryPath, `${JSON.stringify(parsed, null, 2)}\n`, {
107
+ encoding: "utf8",
108
+ mode: PRIVATE_FILE_MODE,
109
+ });
110
+ renameSync(temporaryPath, this.filePath);
111
+ chmodPrivate(this.filePath, PRIVATE_FILE_MODE);
112
+ }
113
+ catch {
114
+ rmSync(temporaryPath, { force: true });
115
+ throw credentialStorageError();
116
+ }
117
+ }
118
+ }
119
+ function resolveOttoHome(env) {
120
+ const configured = env.OTTO_HOME ?? "~/.otto";
121
+ const expanded = configured === "~" ? homedir() : configured.replace(/^~\//u, `${homedir()}/`);
122
+ return path.resolve(expanded);
123
+ }
124
+ function chmodPrivate(target, mode) {
125
+ if (process.platform === "win32")
126
+ return;
127
+ try {
128
+ chmodSync(target, mode);
129
+ }
130
+ catch {
131
+ throw credentialStorageError();
132
+ }
133
+ }
134
+ function isMissingFile(error) {
135
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
136
+ }
137
+ function invalidCredentialFile() {
138
+ return new HubCommandError("HUB_CREDENTIALS_INVALID", "Stored Hub login is invalid. Run `otto hub login <origin>` to replace it.");
139
+ }
140
+ function credentialStorageError() {
141
+ return new HubCommandError("HUB_CREDENTIALS_UNAVAILABLE", "Could not access the private Hub credential store under OTTO_HOME.");
142
+ }
143
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1,27 @@
1
+ export interface HubStatus {
2
+ state: string;
3
+ daemonId: string | null;
4
+ hubOrigin: string | null;
5
+ scopes: string[];
6
+ connectedAt: string | null;
7
+ lastError: string | null;
8
+ }
9
+ export interface HubDaemonClient {
10
+ connectHub(url: string, token: string): Promise<{
11
+ status: HubStatus;
12
+ }>;
13
+ getHubStatus(): Promise<{
14
+ status: HubStatus;
15
+ }>;
16
+ disconnectHub(force: boolean): Promise<{
17
+ status: HubStatus;
18
+ warning?: string;
19
+ }>;
20
+ close(): Promise<void>;
21
+ }
22
+ export interface HubDaemonConnection {
23
+ connect(host: string | undefined): Promise<HubDaemonClient>;
24
+ }
25
+ export declare const productionHubDaemonConnection: HubDaemonConnection;
26
+ export declare function withHubDaemon<T>(connection: HubDaemonConnection, host: string | undefined, action: (client: HubDaemonClient) => Promise<T>): Promise<T>;
27
+ //# sourceMappingURL=daemon-client.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { connectToDaemon } from "../../utils/client.js";
2
+ export const productionHubDaemonConnection = {
3
+ connect: (host) => connectToDaemon({ host }),
4
+ };
5
+ export async function withHubDaemon(connection, host, action) {
6
+ const client = await connection.connect(host);
7
+ try {
8
+ return await action(client);
9
+ }
10
+ finally {
11
+ await client.close().catch(() => undefined);
12
+ }
13
+ }
14
+ //# sourceMappingURL=daemon-client.js.map
@@ -0,0 +1,16 @@
1
+ export interface HubBundleFile {
2
+ path: string;
3
+ content: string;
4
+ }
5
+ export interface HubDeployBundle {
6
+ projectSlug: string;
7
+ files: readonly HubBundleFile[];
8
+ workflowCount: number;
9
+ }
10
+ interface DiscoverHubBundleInput {
11
+ cwd: string;
12
+ project?: string;
13
+ }
14
+ export declare function discoverHubBundle(input: DiscoverHubBundleInput): Promise<HubDeployBundle>;
15
+ export {};
16
+ //# sourceMappingURL=deploy-bundle.d.ts.map
@@ -0,0 +1,248 @@
1
+ import { lstat, readFile, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ import { HubCommandError } from "./error.js";
5
+ const HUB_RESOURCE_PATH = ".otto/hub.yml";
6
+ const LEGACY_TOML_PATH = ".otto/hub.toml";
7
+ const WORKFLOW_DIRECTORY = ".otto/workflows";
8
+ const PARTIAL_DIRECTORY = `${WORKFLOW_DIRECTORY}/partials`;
9
+ const PROJECT_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
10
+ export async function discoverHubBundle(input) {
11
+ const projectSlug = requireProjectSlug(input.project);
12
+ const root = path.resolve(input.cwd);
13
+ await rejectLegacyToml(root);
14
+ const resource = await readBundleFile(root, HUB_RESOURCE_PATH, {
15
+ missingCode: "HUB_RESOURCE_MISSING",
16
+ missingMessage: `${HUB_RESOURCE_PATH} does not exist. Run this command from the project root.`,
17
+ });
18
+ const workflowNames = await discoverWorkflowNames(root);
19
+ const workflows = await Promise.all(workflowNames.map((name) => readBundleFile(root, `${WORKFLOW_DIRECTORY}/${name}`)));
20
+ const partialPaths = collectPartialPaths(workflows);
21
+ const partials = await Promise.all(partialPaths.map((file) => readBundleFile(root, file)));
22
+ const files = [resource, ...workflows, ...partials].toSorted(compareBundleFiles);
23
+ assertUniqueBundlePaths(files);
24
+ return { projectSlug, files, workflowCount: workflows.length };
25
+ }
26
+ function requireProjectSlug(project) {
27
+ if (project === undefined) {
28
+ throw new HubCommandError("HUB_PROJECT_REQUIRED", "Project is required. Pass --project <slug>.");
29
+ }
30
+ if (!PROJECT_SLUG_PATTERN.test(project)) {
31
+ throw new HubCommandError("HUB_INVALID_PROJECT", "Project must be a bare slug such as my-project.");
32
+ }
33
+ return project;
34
+ }
35
+ async function rejectLegacyToml(root) {
36
+ try {
37
+ await lstat(path.join(root, LEGACY_TOML_PATH));
38
+ }
39
+ catch (error) {
40
+ if (errorCode(error) === "ENOENT")
41
+ return;
42
+ throw unreadablePath(LEGACY_TOML_PATH);
43
+ }
44
+ throw new HubCommandError("HUB_CONFIGURATION_EXTENSION_UNSUPPORTED", `${LEGACY_TOML_PATH} is not supported. Use ${HUB_RESOURCE_PATH}.`);
45
+ }
46
+ async function discoverWorkflowNames(root) {
47
+ const directory = path.join(root, WORKFLOW_DIRECTORY);
48
+ await requireSafeDirectory(root, directory, WORKFLOW_DIRECTORY, {
49
+ missingCode: "HUB_WORKFLOW_DIRECTORY_MISSING",
50
+ missingMessage: `${WORKFLOW_DIRECTORY} does not exist.`,
51
+ });
52
+ let entries;
53
+ try {
54
+ entries = await readdir(directory, { withFileTypes: true });
55
+ }
56
+ catch {
57
+ throw unreadablePath(WORKFLOW_DIRECTORY);
58
+ }
59
+ const names = [];
60
+ for (const entry of entries.toSorted((left, right) => compareText(left.name, right.name))) {
61
+ const bundlePath = `${WORKFLOW_DIRECTORY}/${entry.name}`;
62
+ if (entry.isSymbolicLink())
63
+ throw unsafePath(bundlePath);
64
+ if (entry.isDirectory()) {
65
+ if (entry.name === "partials")
66
+ continue;
67
+ throw new HubCommandError("HUB_WORKFLOW_PATH_UNSUPPORTED", `${bundlePath} is not a direct-child .yml workflow.`);
68
+ }
69
+ if (!entry.isFile())
70
+ throw regularFileRequired(bundlePath);
71
+ if (!entry.name.endsWith(".yml")) {
72
+ throw new HubCommandError("HUB_WORKFLOW_EXTENSION_UNSUPPORTED", `${bundlePath} must use the .yml extension.`);
73
+ }
74
+ names.push(entry.name);
75
+ }
76
+ if (names.length === 0) {
77
+ throw new HubCommandError("HUB_WORKFLOW_MISSING", `${WORKFLOW_DIRECTORY} must contain at least one direct-child .yml workflow.`);
78
+ }
79
+ return names;
80
+ }
81
+ function collectPartialPaths(workflows) {
82
+ const paths = new Set();
83
+ for (const workflow of workflows) {
84
+ let document;
85
+ try {
86
+ document = YAML.parse(workflow.content);
87
+ }
88
+ catch {
89
+ throw new HubCommandError("HUB_WORKFLOW_INVALID_YAML", `${workflow.path} is not valid YAML.`);
90
+ }
91
+ if (!isRecord(document) || !Array.isArray(document["steps"]))
92
+ continue;
93
+ for (const step of document["steps"]) {
94
+ if (!isRecord(step) || !Array.isArray(step["prompt"]))
95
+ continue;
96
+ for (const block of step["prompt"]) {
97
+ if (!isRecord(block) || !Object.hasOwn(block, "include"))
98
+ continue;
99
+ const include = block["include"];
100
+ if (typeof include !== "string") {
101
+ throw invalidPartialPath(workflow.path, String(include));
102
+ }
103
+ paths.add(resolvePartialPath(workflow.path, include));
104
+ }
105
+ }
106
+ }
107
+ return [...paths].sort(compareText);
108
+ }
109
+ function resolvePartialPath(workflowPath, include) {
110
+ const decoded = decodePartialPath(workflowPath, include);
111
+ if (/^(?:[a-zA-Z]:[\\/]|[\\/]{1,2})/u.test(decoded)) {
112
+ throw invalidPartialPath(workflowPath, include);
113
+ }
114
+ const segments = decoded.replaceAll("\\", "/").split("/");
115
+ if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..") ||
116
+ segments[0] !== "partials") {
117
+ throw invalidPartialPath(workflowPath, include);
118
+ }
119
+ return `${WORKFLOW_DIRECTORY}/${segments.join("/")}`;
120
+ }
121
+ function decodePartialPath(workflowPath, include) {
122
+ let decoded = include;
123
+ for (let attempt = 0; attempt < 8; attempt += 1) {
124
+ let next;
125
+ try {
126
+ next = decodeURIComponent(decoded);
127
+ }
128
+ catch {
129
+ throw invalidPartialPath(workflowPath, include);
130
+ }
131
+ if (next === decoded)
132
+ break;
133
+ decoded = next;
134
+ }
135
+ if (decoded.includes("\0") || /%[0-9a-f]{2}/iu.test(decoded)) {
136
+ throw invalidPartialPath(workflowPath, include);
137
+ }
138
+ return decoded;
139
+ }
140
+ async function readBundleFile(root, bundlePath, missing = {
141
+ missingCode: "HUB_BUNDLE_FILE_MISSING",
142
+ missingMessage: `${bundlePath} does not exist.`,
143
+ }) {
144
+ const absolutePath = path.join(root, ...bundlePath.split("/"));
145
+ await rejectSymlinkComponents(root, absolutePath, bundlePath);
146
+ let stats;
147
+ try {
148
+ stats = await lstat(absolutePath);
149
+ }
150
+ catch (error) {
151
+ if (errorCode(error) === "ENOENT") {
152
+ throw new HubCommandError(missing.missingCode, missing.missingMessage);
153
+ }
154
+ throw unreadablePath(bundlePath);
155
+ }
156
+ if (stats.isSymbolicLink())
157
+ throw unsafePath(bundlePath);
158
+ if (!stats.isFile())
159
+ throw regularFileRequired(bundlePath);
160
+ if (!hasReadPermission(stats.mode))
161
+ throw unreadablePath(bundlePath);
162
+ try {
163
+ return { path: bundlePath, content: (await readFile(absolutePath)).toString("utf8") };
164
+ }
165
+ catch {
166
+ throw unreadablePath(bundlePath);
167
+ }
168
+ }
169
+ async function requireSafeDirectory(root, absolutePath, bundlePath, missing) {
170
+ await rejectSymlinkComponents(root, absolutePath, bundlePath);
171
+ let stats;
172
+ try {
173
+ stats = await lstat(absolutePath);
174
+ }
175
+ catch (error) {
176
+ if (errorCode(error) === "ENOENT") {
177
+ throw new HubCommandError(missing.missingCode, missing.missingMessage);
178
+ }
179
+ throw unreadablePath(bundlePath);
180
+ }
181
+ if (stats.isSymbolicLink())
182
+ throw unsafePath(bundlePath);
183
+ if (!stats.isDirectory()) {
184
+ throw new HubCommandError("HUB_WORKFLOW_DIRECTORY_INVALID", `${bundlePath} must be a directory.`);
185
+ }
186
+ }
187
+ async function rejectSymlinkComponents(root, target, bundlePath) {
188
+ const relative = path.relative(root, target);
189
+ let current = root;
190
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
191
+ current = path.join(current, segment);
192
+ try {
193
+ if ((await lstat(current)).isSymbolicLink())
194
+ throw unsafePath(bundlePath);
195
+ }
196
+ catch (error) {
197
+ if (error instanceof HubCommandError)
198
+ throw error;
199
+ if (errorCode(error) === "ENOENT")
200
+ return;
201
+ throw unreadablePath(bundlePath);
202
+ }
203
+ }
204
+ }
205
+ function assertUniqueBundlePaths(files) {
206
+ const seen = new Set();
207
+ for (const file of files) {
208
+ if (seen.has(file.path)) {
209
+ throw new HubCommandError("HUB_BUNDLE_PATH_DUPLICATE", `${file.path} appears more than once in the discovered bundle.`);
210
+ }
211
+ seen.add(file.path);
212
+ }
213
+ }
214
+ function invalidPartialPath(workflowPath, include) {
215
+ return new HubCommandError("HUB_PARTIAL_PATH_INVALID", `${workflowPath} includes an unsafe path outside ${PARTIAL_DIRECTORY}: ${include}`);
216
+ }
217
+ function unsafePath(bundlePath) {
218
+ return new HubCommandError("HUB_BUNDLE_UNSAFE_PATH", `${bundlePath} must not use a symlink.`);
219
+ }
220
+ function unreadablePath(bundlePath) {
221
+ return new HubCommandError("HUB_BUNDLE_UNREADABLE", `Could not read ${bundlePath}. Check the file and permissions.`);
222
+ }
223
+ function regularFileRequired(bundlePath) {
224
+ return new HubCommandError("HUB_BUNDLE_NOT_FILE", `${bundlePath} must be a regular file.`);
225
+ }
226
+ function compareBundleFiles(left, right) {
227
+ return compareText(left.path, right.path);
228
+ }
229
+ function compareText(left, right) {
230
+ if (left < right)
231
+ return -1;
232
+ if (left > right)
233
+ return 1;
234
+ return 0;
235
+ }
236
+ function hasReadPermission(mode) {
237
+ return (mode & 0o444) !== 0;
238
+ }
239
+ function errorCode(error) {
240
+ if (typeof error !== "object" || error === null)
241
+ return undefined;
242
+ const code = Reflect.get(error, "code");
243
+ return typeof code === "string" ? code : undefined;
244
+ }
245
+ function isRecord(value) {
246
+ return typeof value === "object" && value !== null && !Array.isArray(value);
247
+ }
248
+ //# sourceMappingURL=deploy-bundle.js.map
@@ -0,0 +1,40 @@
1
+ import type { Command } from "commander";
2
+ import { type SingleResult } from "../../output/index.js";
3
+ import { HubHttpClient, type HubInstallResult, type HubValidationResult } from "./hub-client/index.js";
4
+ import { type HubCredentialStore } from "./credentials.js";
5
+ import { type HubDeployBundle } from "./deploy-bundle.js";
6
+ import { type HubReporter } from "./reporter.js";
7
+ export interface HubDeployOptions {
8
+ project?: string;
9
+ hub?: string;
10
+ apiKey?: string;
11
+ dryRun?: boolean;
12
+ json?: boolean;
13
+ }
14
+ export interface HubDeployEnvironment {
15
+ cwd: string;
16
+ env: Readonly<Record<string, string | undefined>>;
17
+ credentials?: HubCredentialStore;
18
+ hub?: Pick<HubHttpClient, "installConfiguration" | "validateConfiguration">;
19
+ reporter?: HubReporter;
20
+ }
21
+ export interface HubDeployCommandDependencies {
22
+ env: Readonly<Record<string, string | undefined>>;
23
+ credentials: HubCredentialStore;
24
+ hub: Pick<HubHttpClient, "installConfiguration" | "validateConfiguration">;
25
+ reporter: HubReporter;
26
+ cwd(): string;
27
+ }
28
+ interface HubDeployResult extends HubInstallResult {
29
+ origin: string;
30
+ workflows: number;
31
+ }
32
+ interface HubDryRunResult extends HubValidationResult {
33
+ origin: string;
34
+ workflows: number;
35
+ }
36
+ export declare function runHubDeploy(options: HubDeployOptions, environment?: HubDeployEnvironment): Promise<SingleResult<HubDeployResult> | SingleResult<HubDryRunResult>>;
37
+ export declare function runHubDeployBundle(options: HubDeployOptions, deployInput: HubDeployBundle, environment: HubDeployEnvironment): Promise<SingleResult<HubDeployResult> | SingleResult<HubDryRunResult>>;
38
+ export declare function addHubDeployCommand(hub: Command, dependencies: HubDeployCommandDependencies): void;
39
+ export {};
40
+ //# sourceMappingURL=deploy.d.ts.map
@@ -0,0 +1,90 @@
1
+ import { withOutput } from "../../output/index.js";
2
+ import { addJsonOption } from "../../utils/command-options.js";
3
+ import { resolveHubCredential, resolveHubOrigin } from "./authority.js";
4
+ import { HubHttpClient, } from "./hub-client/index.js";
5
+ import { PrivateHubCredentialStore } from "./credentials.js";
6
+ import { discoverHubBundle } from "./deploy-bundle.js";
7
+ import { processHubReporter, reportHubProgress } from "./reporter.js";
8
+ import { addHubResolutionHelp } from "./help.js";
9
+ const resultSchema = {
10
+ idField: "versionId",
11
+ columns: [
12
+ { header: "PROJECT", field: "projectSlug" },
13
+ { header: "VERSION", field: "version" },
14
+ { header: "VERSION ID", field: "versionId" },
15
+ { header: "ACTIVE", field: "active" },
16
+ { header: "WORKFLOWS", field: "workflows" },
17
+ { header: "HUB", field: "origin" },
18
+ ],
19
+ };
20
+ const validationSchema = {
21
+ idField: "projectSlug",
22
+ columns: [
23
+ { header: "PROJECT", field: "projectSlug" },
24
+ { header: "VALID", field: "valid" },
25
+ { header: "WORKFLOWS", field: "workflows" },
26
+ { header: "HUB", field: "origin" },
27
+ ],
28
+ };
29
+ export async function runHubDeploy(options, environment = {
30
+ cwd: process.cwd(),
31
+ env: process.env,
32
+ credentials: new PrivateHubCredentialStore(),
33
+ hub: new HubHttpClient(),
34
+ }) {
35
+ const deployInput = await discoverHubBundle({
36
+ cwd: environment.cwd,
37
+ ...(options.project === undefined ? {} : { project: options.project }),
38
+ });
39
+ return runHubDeployBundle(options, deployInput, environment);
40
+ }
41
+ export async function runHubDeployBundle(options, deployInput, environment) {
42
+ const credentials = environment.credentials ?? new PrivateHubCredentialStore(environment.env);
43
+ const resolution = {
44
+ options: { origin: options.hub, apiKey: options.apiKey },
45
+ env: environment.env,
46
+ credentials,
47
+ };
48
+ const origin = resolveHubOrigin(resolution);
49
+ const action = options.dryRun === true ? "Validating" : "Deploying";
50
+ reportHubProgress(environment.reporter ?? processHubReporter, options, `${action} ${deployInput.projectSlug} ${options.dryRun === true ? "against" : "to"} ${origin}`);
51
+ const credential = resolveHubCredential({ ...resolution, origin });
52
+ const request = {
53
+ origin,
54
+ apiKey: credential,
55
+ ...deployInput,
56
+ };
57
+ if (options.dryRun === true) {
58
+ const validated = await (environment.hub ?? new HubHttpClient()).validateConfiguration(request);
59
+ return {
60
+ type: "single",
61
+ data: { ...validated, workflows: deployInput.workflowCount, origin },
62
+ schema: validationSchema,
63
+ };
64
+ }
65
+ const deployed = await (environment.hub ?? new HubHttpClient()).installConfiguration(request);
66
+ return {
67
+ type: "single",
68
+ data: { ...deployed, workflows: deployInput.workflowCount, origin },
69
+ schema: resultSchema,
70
+ };
71
+ }
72
+ export function addHubDeployCommand(hub, dependencies) {
73
+ addJsonOption(addHubResolutionHelp(hub
74
+ .command("deploy")
75
+ .description("Discover, validate, and activate the canonical .otto Hub bundle")
76
+ .option("-p, --project <slug>", "Target project slug")
77
+ .option("--hub <origin>", "Otto Hub origin")
78
+ .option("--api-key <secret>", "Organization API key")
79
+ .option("--dry-run", "Validate without installing or activating"))).action(withOutput(async (...args) => {
80
+ const options = args.at(-2);
81
+ return runHubDeploy(options, {
82
+ cwd: dependencies.cwd(),
83
+ env: dependencies.env,
84
+ credentials: dependencies.credentials,
85
+ hub: dependencies.hub,
86
+ reporter: dependencies.reporter,
87
+ });
88
+ }));
89
+ }
90
+ //# sourceMappingURL=deploy.js.map
@@ -0,0 +1,16 @@
1
+ import type { Command } from "commander";
2
+ import type { HubDaemonConnection } from "./daemon-client.js";
3
+ import { type HubReporter } from "./reporter.js";
4
+ interface HubDisconnectOptions {
5
+ force?: boolean;
6
+ host?: string;
7
+ json?: boolean;
8
+ }
9
+ interface HubDisconnectDependencies {
10
+ daemon: HubDaemonConnection;
11
+ reporter: HubReporter;
12
+ }
13
+ export declare function runHubDisconnect(options: HubDisconnectOptions, dependencies: HubDisconnectDependencies): Promise<import("../../output/types.js").ListResult<import("./status-output.js").HubRow>>;
14
+ export declare function addHubDisconnectCommand(parent: Command, dependencies: HubDisconnectDependencies): void;
15
+ export {};
16
+ //# sourceMappingURL=disconnect.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { withOutput } from "../../output/index.js";
2
+ import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
3
+ import { withHubDaemon } from "./daemon-client.js";
4
+ import { reportHubProgress } from "./reporter.js";
5
+ import { hubStatusResult } from "./status-output.js";
6
+ export function runHubDisconnect(options, dependencies) {
7
+ return withHubDaemon(dependencies.daemon, options.host, async (client) => {
8
+ const current = (await client.getHubStatus()).status;
9
+ if (current.hubOrigin !== null) {
10
+ reportHubProgress(dependencies.reporter, options, `Disconnecting this daemon from ${current.hubOrigin}`);
11
+ }
12
+ const response = await client.disconnectHub(options.force ?? false);
13
+ return hubStatusResult(response.status, response.warning);
14
+ });
15
+ }
16
+ export function addHubDisconnectCommand(parent, dependencies) {
17
+ addJsonAndDaemonHostOptions(parent
18
+ .command("disconnect")
19
+ .option("--force", "Remove local authority without notifying the Hub")).action(withOutput(async (...args) => {
20
+ const options = args.at(-2);
21
+ return runHubDisconnect(options, dependencies);
22
+ }));
23
+ }
24
+ //# sourceMappingURL=disconnect.js.map
@@ -0,0 +1,6 @@
1
+ export declare class HubCommandError extends Error {
2
+ readonly code: string;
3
+ readonly details?: string | undefined;
4
+ constructor(code: string, message: string, details?: string | undefined);
5
+ }
6
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1,9 @@
1
+ export class HubCommandError extends Error {
2
+ constructor(code, message, details) {
3
+ super(message);
4
+ this.code = code;
5
+ this.details = details;
6
+ this.name = "HubCommandError";
7
+ }
8
+ }
9
+ //# sourceMappingURL=error.js.map
@@ -0,0 +1,3 @@
1
+ import type { Command } from "commander";
2
+ export declare function addHubResolutionHelp(command: Command): Command;
3
+ //# sourceMappingURL=help.d.ts.map
@@ -0,0 +1,5 @@
1
+ const resolutionHelp = "\nHub origin precedence: command origin/--hub, OTTO_HUB_URL, active stored login, then https://hub.otto-code.me.\nCredential precedence: --api-key, OTTO_HUB_API_KEY, then a stored login for the exact resolved origin.\n";
2
+ export function addHubResolutionHelp(command) {
3
+ return command.addHelpText("after", resolutionHelp);
4
+ }
5
+ //# sourceMappingURL=help.js.map
@@ -0,0 +1,19 @@
1
+ import type { HubBundleFile } from "../deploy-bundle.js";
2
+ import { type CliAuthorization, type CliAuthorizationPoll, type HubInstallResult, type HubConfigurationResources, type HubProject, type HubValidationResult } from "./internal/contracts.js";
3
+ export type { CliAuthorization, CliAuthorizationPoll, HubInstallResult, HubConfigurationResources, HubProject, HubValidationResult, } from "./internal/contracts.js";
4
+ interface HubConfigurationInput {
5
+ origin: string;
6
+ apiKey: string;
7
+ projectSlug: string;
8
+ files: readonly HubBundleFile[];
9
+ }
10
+ export declare class HubHttpClient {
11
+ startCliAuthorization(origin: string): Promise<CliAuthorization>;
12
+ pollCliAuthorization(origin: string, deviceCode: string, timeoutMilliseconds: number): Promise<CliAuthorizationPoll>;
13
+ listProjects(origin: string, apiKey: string): Promise<HubProject[]>;
14
+ listConfigurationResources(origin: string, apiKey: string): Promise<HubConfigurationResources>;
15
+ installConfiguration(input: HubConfigurationInput): Promise<HubInstallResult>;
16
+ validateConfiguration(input: HubConfigurationInput): Promise<HubValidationResult>;
17
+ issueEnrollmentToken(origin: string, apiKey: string): Promise<string>;
18
+ }
19
+ //# sourceMappingURL=index.d.ts.map