@ninedeploy/sdk 0.10.5 → 0.16.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 (129) hide show
  1. package/LICENSE +21 -21
  2. package/dist/apiPath.d.ts +5 -0
  3. package/dist/apiPath.d.ts.map +1 -0
  4. package/dist/apiPath.js +18 -0
  5. package/dist/apiPath.js.map +1 -0
  6. package/dist/client.d.ts +29 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +51 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/databaseImport.d.ts +138 -0
  11. package/dist/databaseImport.d.ts.map +1 -0
  12. package/dist/databaseImport.js +126 -0
  13. package/dist/databaseImport.js.map +1 -0
  14. package/dist/errors.js +1 -1
  15. package/dist/errors.js.map +1 -1
  16. package/dist/index.d.ts +12 -1973
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +9 -641
  19. package/dist/index.js.map +1 -1
  20. package/dist/manifest.d.ts.map +1 -1
  21. package/dist/manifest.js +25 -21
  22. package/dist/manifest.js.map +1 -1
  23. package/dist/resources/auth.d.ts +282 -0
  24. package/dist/resources/auth.d.ts.map +1 -0
  25. package/dist/resources/auth.js +111 -0
  26. package/dist/resources/auth.js.map +1 -0
  27. package/dist/resources/databases.d.ts +178 -0
  28. package/dist/resources/databases.d.ts.map +1 -0
  29. package/dist/resources/databases.js +82 -0
  30. package/dist/resources/databases.js.map +1 -0
  31. package/dist/resources/deploys.d.ts +125 -0
  32. package/dist/resources/deploys.d.ts.map +1 -0
  33. package/dist/resources/deploys.js +46 -0
  34. package/dist/resources/deploys.js.map +1 -0
  35. package/dist/resources/domains.d.ts +101 -0
  36. package/dist/resources/domains.d.ts.map +1 -0
  37. package/dist/resources/domains.js +28 -0
  38. package/dist/resources/domains.js.map +1 -0
  39. package/dist/resources/env.d.ts +55 -0
  40. package/dist/resources/env.d.ts.map +1 -0
  41. package/dist/resources/env.js +33 -0
  42. package/dist/resources/env.js.map +1 -0
  43. package/dist/resources/logs.d.ts +27 -0
  44. package/dist/resources/logs.d.ts.map +1 -0
  45. package/dist/resources/logs.js +18 -0
  46. package/dist/resources/logs.js.map +1 -0
  47. package/dist/resources/network.d.ts +134 -0
  48. package/dist/resources/network.d.ts.map +1 -0
  49. package/dist/resources/network.js +33 -0
  50. package/dist/resources/network.js.map +1 -0
  51. package/dist/resources/plugins.d.ts +42 -0
  52. package/dist/resources/plugins.d.ts.map +1 -0
  53. package/dist/resources/plugins.js +22 -0
  54. package/dist/resources/plugins.js.map +1 -0
  55. package/dist/resources/servers.d.ts +130 -0
  56. package/dist/resources/servers.d.ts.map +1 -0
  57. package/dist/resources/servers.js +37 -0
  58. package/dist/resources/servers.js.map +1 -0
  59. package/dist/resources/services.d.ts +185 -0
  60. package/dist/resources/services.d.ts.map +1 -0
  61. package/dist/resources/services.js +83 -0
  62. package/dist/resources/services.js.map +1 -0
  63. package/dist/resources/settings.d.ts +312 -0
  64. package/dist/resources/settings.d.ts.map +1 -0
  65. package/dist/resources/settings.js +85 -0
  66. package/dist/resources/settings.js.map +1 -0
  67. package/dist/resources/sources.d.ts +87 -0
  68. package/dist/resources/sources.d.ts.map +1 -0
  69. package/dist/resources/sources.js +39 -0
  70. package/dist/resources/sources.js.map +1 -0
  71. package/dist/resources/system.d.ts +240 -0
  72. package/dist/resources/system.d.ts.map +1 -0
  73. package/dist/resources/system.js +86 -0
  74. package/dist/resources/system.js.map +1 -0
  75. package/dist/resources/templates.d.ts +36 -0
  76. package/dist/resources/templates.d.ts.map +1 -0
  77. package/dist/resources/templates.js +17 -0
  78. package/dist/resources/templates.js.map +1 -0
  79. package/dist/resources/terminals.d.ts +60 -0
  80. package/dist/resources/terminals.d.ts.map +1 -0
  81. package/dist/resources/terminals.js +35 -0
  82. package/dist/resources/terminals.js.map +1 -0
  83. package/dist/resources/traefik.d.ts +84 -0
  84. package/dist/resources/traefik.d.ts.map +1 -0
  85. package/dist/resources/traefik.js +35 -0
  86. package/dist/resources/traefik.js.map +1 -0
  87. package/dist/resources/volumes.d.ts +114 -0
  88. package/dist/resources/volumes.d.ts.map +1 -0
  89. package/dist/resources/volumes.js +47 -0
  90. package/dist/resources/volumes.js.map +1 -0
  91. package/dist/terminal.d.ts +98 -0
  92. package/dist/terminal.d.ts.map +1 -0
  93. package/dist/terminal.js +180 -0
  94. package/dist/terminal.js.map +1 -0
  95. package/dist/transport.d.ts +61 -0
  96. package/dist/transport.d.ts.map +1 -0
  97. package/dist/transport.js +90 -0
  98. package/dist/transport.js.map +1 -0
  99. package/dist/types.d.ts +539 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +5 -0
  102. package/dist/types.js.map +1 -0
  103. package/package.json +5 -4
  104. package/src/apiPath.ts +23 -0
  105. package/src/client.ts +70 -0
  106. package/src/databaseImport.ts +174 -0
  107. package/src/errors.ts +27 -0
  108. package/src/index.ts +118 -0
  109. package/src/manifest.ts +414 -0
  110. package/src/resources/auth.ts +357 -0
  111. package/src/resources/databases.ts +227 -0
  112. package/src/resources/deploys.ts +161 -0
  113. package/src/resources/domains.ts +142 -0
  114. package/src/resources/env.ts +84 -0
  115. package/src/resources/logs.ts +48 -0
  116. package/src/resources/network.ts +126 -0
  117. package/src/resources/plugins.ts +52 -0
  118. package/src/resources/servers.ts +141 -0
  119. package/src/resources/services.ts +250 -0
  120. package/src/resources/settings.ts +275 -0
  121. package/src/resources/sources.ts +131 -0
  122. package/src/resources/system.ts +281 -0
  123. package/src/resources/templates.ts +55 -0
  124. package/src/resources/terminals.ts +112 -0
  125. package/src/resources/traefik.ts +116 -0
  126. package/src/resources/volumes.ts +138 -0
  127. package/src/terminal.ts +254 -0
  128. package/src/transport.ts +154 -0
  129. package/src/types.ts +588 -0
@@ -0,0 +1,141 @@
1
+ // Client surface: servers, swarm, orchestrators.
2
+ // Split out of the former single-file SDK entry (src/index.ts); the public
3
+ // surface is re-exported unchanged from there.
4
+ import type {
5
+ ServerSshTest,
6
+ ServerSshTestResult,
7
+ ServerSshBootstrap,
8
+ ServerBootstrapResult,
9
+ ServerRolesInput,
10
+ SwarmInitInput,
11
+ SwarmSettingsInput,
12
+ SwarmStatus,
13
+ } from '@ninedeploy/schemas';
14
+ import type { ServerListEntry, ServerRolesResult, SwarmJoinResult, SwarmLeaveResult } from '../types.js';
15
+ import type { Transport } from '../transport.js';
16
+
17
+ /** Long Swarm calls: a leave drains for up to 5 minutes; init and join run Docker on two hosts. */
18
+ const SWARM_CALL_TIMEOUT_MS = 10 * 60_000;
19
+
20
+ export interface ServersApi {
21
+ servers: {
22
+ /**
23
+ * 0.15: `terminal` says what the node's agent offers terminals (absent on
24
+ * a 0.14 panel). `container: false` with a `reason` usually means the
25
+ * agent predates v0.15.0.
26
+ */
27
+ list: () => Promise<ServerListEntry[]>;
28
+ /** Register a server — the agent token + its sha256 are returned exactly once. */
29
+ create: (input: { name: string; host: string; port?: number }) => Promise<{ id: number; token: string; tokenSha256: string; agentCommand: string }>;
30
+ remove: (id: number, options?: { force?: boolean }) => Promise<{ ok: boolean }>;
31
+ test: (id: number) => Promise<{ ok: boolean; status: string }>;
32
+ approve: (id: number) => Promise<{ ok: boolean; status: string }>;
33
+ reject: (id: number) => Promise<{ ok: boolean }>;
34
+ sshTest: (input: ServerSshTest) => Promise<ServerSshTestResult>;
35
+ sshBootstrap: (input: ServerSshBootstrap) => Promise<ServerBootstrapResult>;
36
+ bootstrapLogs: (id: number) => Promise<{ logs: string[] }>;
37
+ /** r467: live host + per-container telemetry from a node's agent. */
38
+ stats: (id: number) => Promise<{
39
+ host: { cpuCores: number; load1: number; memTotalBytes: number; memUsedBytes: number } | null;
40
+ disk: { totalBytes: number; usedBytes: number };
41
+ containers: Array<{ name: string; kind: 'service'; refId: number; refName: string; cpuPct: number; memMb: number; memLimitMb: number }>;
42
+ }>;
43
+ // ── 0.16 T8 surfaces ──
44
+ /** The build-server role (`PATCH /v1/servers/:id`, operator). Partial. */
45
+ update: (id: number, input: ServerRolesInput) => Promise<ServerRolesResult>;
46
+ /**
47
+ * Join the node to the panel host's swarm as a worker (operator). 422
48
+ * `node_agent_outdated` for an older agent, 422 `node_swarm_not_enabled`
49
+ * until the node's owner sets {@link AGENT_SWARM_MANAGER_VAR}, 409
50
+ * `swarm_already_joined` / `swarm_not_manager`, 502
51
+ * `swarm_overlay_unavailable` / `swarm_node_unverified`.
52
+ */
53
+ swarmJoin: (id: number) => Promise<SwarmJoinResult>;
54
+ /** Drain the node and take it out of the swarm (operator; can take minutes). 409 `swarm_not_joined`. */
55
+ swarmLeave: (id: number) => Promise<SwarmLeaveResult>;
56
+ // ── end 0.16 T8 ──
57
+ };
58
+ // ── end 0.15 T6 ──
59
+ // ── 0.16 T8 surfaces ──
60
+ /**
61
+ * Swarm on the panel host (operator only). `init`, and enabling through
62
+ * `settings`, need step-up: an interactive session plus the account
63
+ * `password` (or a sign-in within 10 minutes). Join tokens are never returned.
64
+ */
65
+ swarm: {
66
+ get: () => Promise<SwarmStatus>;
67
+ /** `docker swarm init` on the panel host. 409 `swarm_already_active`; 502 `swarm_overlay_unavailable`. */
68
+ init: (input: SwarmInitInput) => Promise<SwarmStatus>;
69
+ /** Enable (step-up; 409 `swarm_not_manager`) or disable Swarm deploys. */
70
+ settings: (input: SwarmSettingsInput) => Promise<SwarmStatus>;
71
+ };
72
+ orchestrators: {
73
+ /** Every `IOrchestrator` the running kernel has registered. G-10 PR-A. */
74
+ list: () => Promise<{
75
+ orchestrators: Array<{ name: string; stacks: Array<{ name: string; serviceCount: number }> }>;
76
+ }>;
77
+ /** The stacks one orchestrator knows about. */
78
+ stacks: (orchestrator: string) => Promise<{
79
+ stacks: Array<{ name: string; serviceCount: number }>;
80
+ }>;
81
+ /**
82
+ * Stable snapshot of ONE stack. Returns `null` when the orchestrator has
83
+ * no record of it.
84
+ *
85
+ * r036: this used to take only the orchestrator name and hit
86
+ * `/:name/stacks`, which passed that name through as the STACK name — so
87
+ * it answered `null` for every stack not coincidentally named after its
88
+ * orchestrator. The stack now has its own argument and path segment.
89
+ */
90
+ stackStatus: (orchestrator: string, stack: string) => Promise<{
91
+ name: string;
92
+ services: Array<{ name: string; state: 'running' | 'stopped' | 'partial' | 'unknown'; replicas: number }>;
93
+ appliedAt: string;
94
+ } | null>;
95
+ };
96
+ }
97
+
98
+ export function serversApi(t: Transport): ServersApi {
99
+ const { get, send, request } = t;
100
+ return {
101
+ servers: {
102
+ list: () => get('/v1/servers'),
103
+ create: (input) => send('POST', '/v1/servers', input),
104
+ remove: (id, opts) => send<{ ok: boolean }>('DELETE', `/v1/servers/${id}${opts?.force ? '?force=true' : ''}`),
105
+ test: (id) => send<{ ok: boolean; status: string }>('POST', `/v1/servers/${id}/test`),
106
+ approve: (id) => send<{ ok: boolean; status: string }>('POST', `/v1/servers/${id}/approve`),
107
+ reject: (id) => send<{ ok: boolean }>('POST', `/v1/servers/${id}/reject`),
108
+ sshTest: (input) => send<ServerSshTestResult>('POST', '/v1/servers/ssh-test', input),
109
+ sshBootstrap: (input) => send<ServerBootstrapResult>('POST', '/v1/servers/ssh-bootstrap', input),
110
+ bootstrapLogs: (id) => get<{ logs: string[] }>(`/v1/servers/${id}/bootstrap-logs`),
111
+ stats: (id) => get(`/v1/servers/${id}/stats`),
112
+ // ── 0.16 T8 surfaces ──
113
+ update: (id, input) => send<ServerRolesResult>('PATCH', `/v1/servers/${id}`, input),
114
+ swarmJoin: (id) => request<SwarmJoinResult>(`/v1/servers/${id}/swarm/join`, { method: 'POST' }, SWARM_CALL_TIMEOUT_MS),
115
+ swarmLeave: (id) => request<SwarmLeaveResult>(`/v1/servers/${id}/swarm/leave`, { method: 'POST' }, SWARM_CALL_TIMEOUT_MS),
116
+ // ── end 0.16 T8 ──
117
+ },
118
+ // ── end 0.15 T6 ──
119
+ // ── 0.16 T8 surfaces ──
120
+ swarm: {
121
+ get: () => get<SwarmStatus>('/v1/swarm'),
122
+ init: (input) => request<SwarmStatus>('/v1/swarm/init', { method: 'POST', body: input }, SWARM_CALL_TIMEOUT_MS),
123
+ settings: (input) => send<SwarmStatus>('PUT', '/v1/swarm/settings', input),
124
+ },
125
+ orchestrators: {
126
+ list: () => get<{
127
+ orchestrators: Array<{ name: string; stacks: Array<{ name: string; serviceCount: number }> }>;
128
+ }>('/v1/orchestrators'),
129
+ stacks: (orchestrator) => get<{
130
+ stacks: Array<{ name: string; serviceCount: number }>;
131
+ }>(`/v1/orchestrators/${encodeURIComponent(orchestrator)}/stacks`),
132
+ stackStatus: (orchestrator, stack) => get<{
133
+ name: string;
134
+ services: Array<{ name: string; state: 'running' | 'stopped' | 'partial' | 'unknown'; replicas: number }>;
135
+ appliedAt: string;
136
+ } | null>(
137
+ `/v1/orchestrators/${encodeURIComponent(orchestrator)}/stacks/${encodeURIComponent(stack)}`,
138
+ ),
139
+ },
140
+ };
141
+ }
@@ -0,0 +1,250 @@
1
+ // Client surface: services, labels, environments, serviceTags, projects, fanout, limits, attachments.
2
+ // Split out of the former single-file SDK entry (src/index.ts); the public
3
+ // surface is re-exported unchanged from there.
4
+ import type {
5
+ Attachment,
6
+ ComposePreviewRequestInput,
7
+ ComposePreviewResponse,
8
+ CreateProjectInput,
9
+ CreateServiceInput,
10
+ Label,
11
+ CreateLabelInput,
12
+ LabelPatchInput,
13
+ Environment,
14
+ EnvironmentCreateInput,
15
+ ProjectEntry,
16
+ ProjectPatchInput,
17
+ Service,
18
+ ServiceTags,
19
+ SetServiceTagsInput,
20
+ SetLimitsInput,
21
+ UpdateServiceInput,
22
+ ServiceGithubFeedbackPatch,
23
+ ServiceGithubLink,
24
+ ServiceGithubLinkPut,
25
+ ServiceGithubStatus,
26
+ ImageTransfer,
27
+ ServicePlacementInput,
28
+ ServicePlacementView,
29
+ ServiceSwarmStatus,
30
+ } from '@ninedeploy/schemas';
31
+ import { apiGetUrl } from '../apiPath.js';
32
+ import type { ApplyManifestResult, ApplyManifestInput, ServiceDetail } from '../types.js';
33
+ import type { Transport } from '../transport.js';
34
+
35
+ export interface ServicesApi {
36
+ services: {
37
+ /**
38
+ * `query` is appended verbatim. Use `?tagWorkspaceIds=1,2&tagProjectIds=3`
39
+ * to scope the list by tag (each dimension ANDs, members within a
40
+ * dimension OR). The legacy `?projectId=` query is no longer accepted.
41
+ */
42
+ list: (query?: string) => Promise<Service[]>;
43
+ /** 0.16 T8: the detail response also carries `placement` (absent on older panels). */
44
+ get: (id: number) => Promise<ServiceDetail>;
45
+ create: (input: CreateServiceInput) => Promise<Service>;
46
+ update: (id: number, input: UpdateServiceInput) => Promise<Service>;
47
+ /**
48
+ * Dry-run a compose file before creating anything: blocking reasons,
49
+ * host-privilege warnings, the declared services and the variables the
50
+ * stack will ask for. Admin-only, and writes nothing.
51
+ */
52
+ composePreview: (input: ComposePreviewRequestInput) => Promise<ComposePreviewResponse>;
53
+ remove: (id: number) => Promise<void>;
54
+ stop: (id: number) => Promise<{ ok: boolean; status: string }>;
55
+ start: (id: number) => Promise<{ ok: boolean; status: string }>;
56
+ restart: (id: number) => Promise<{ ok: boolean; status: string }>;
57
+ logs: (id: number) => Promise<{ lines: string }>;
58
+ clone: (id: number, input?: { name?: string; slug?: string }) => Promise<Service>;
59
+ exportUrl: (id: number) => string;
60
+ importBundle: (bundle: unknown) => Promise<{ ok: boolean; serviceId: number; slug: string; message: string }>;
61
+ /**
62
+ * `.ninedeploy` manifest endpoints. The server applies
63
+ * build / run / network sections to the service + build
64
+ * config rows (operator > manifest > DB merge semantics);
65
+ * routes / alerts / database reconcile happens at deploy
66
+ * time via the deploy pipeline and is intentionally
67
+ * outside this endpoint's scope.
68
+ */
69
+ manifest: {
70
+ /**
71
+ * Push a parsed manifest to the panel and reconcile it
72
+ * into the service. Requires the `admin` role on the
73
+ * service. The response echoes the diff so the CLI can
74
+ * render a one-shot summary without a follow-up GET.
75
+ */
76
+ apply: (serviceId: number, input: ApplyManifestInput) => Promise<ApplyManifestResult>;
77
+ };
78
+ /**
79
+ * The service's GitHub App link (0.13). `get` is viewer; `feedback` is
80
+ * service admin; `link`, `migrate`, `finalize` and `unlink` are operator.
81
+ * `unlink` answers 409 when the service clones through the App source
82
+ * itself (attach another source first, or disable the link).
83
+ */
84
+ github: {
85
+ get: (id: number) => Promise<ServiceGithubStatus>;
86
+ link: (id: number, input: ServiceGithubLinkPut) => Promise<{ link: ServiceGithubLink }>;
87
+ feedback: (id: number, input: ServiceGithubFeedbackPatch) => Promise<{ link: ServiceGithubLink }>;
88
+ migrate: (id: number, sourceId: number) => Promise<{ link: ServiceGithubLink }>;
89
+ finalize: (id: number) => Promise<{ link: ServiceGithubLink; webhooksDeactivated: number }>;
90
+ unlink: (id: number) => Promise<{ ok: boolean; sourceId: number | null; webhooksReactivated: number }>;
91
+ };
92
+ // ── 0.16 T8 surfaces ──
93
+ /**
94
+ * Build placement (multi-node): where the image is built, how it travels
95
+ * and which orchestrator runs it. `get` is any seat on the service; `set`
96
+ * is operator only and partial (only the named keys change; `null`
97
+ * restores the 0.15 default). An older node agent answers 422
98
+ * `node_agent_outdated`; switching to Swarm answers 422 `swarm_unsupported`
99
+ * or `swarm_disabled`.
100
+ */
101
+ placement: {
102
+ get: (id: number) => Promise<ServicePlacementView>;
103
+ set: (id: number, input: ServicePlacementInput) => Promise<ServicePlacementView>;
104
+ };
105
+ /** Image transfers of the service, newest first (`limit` 1-100, default 20). */
106
+ imageTransfers: (id: number, query?: { limit?: number }) => Promise<ImageTransfer[]>;
107
+ /** Swarm tasks of the service; `stack: null` when it is not on Swarm. */
108
+ swarm: (id: number) => Promise<ServiceSwarmStatus>;
109
+ // ── end 0.16 T8 ──
110
+ };
111
+ labels: {
112
+ /** `query` is appended verbatim, e.g. `?workspaceId=1`. */
113
+ list: (query?: string) => Promise<Label[]>;
114
+ create: (input: CreateLabelInput) => Promise<Label>;
115
+ update: (id: number, input: LabelPatchInput) => Promise<Label>;
116
+ remove: (id: number) => Promise<{ ok: boolean }>;
117
+ };
118
+ /** Deployment lanes (production / staging / development) per workspace. */
119
+ environments: {
120
+ list: () => Promise<Environment[]>;
121
+ create: (input: EnvironmentCreateInput) => Promise<Environment>;
122
+ rename: (id: number, name: string) => Promise<Environment>;
123
+ remove: (id: number) => Promise<{ ok: boolean }>;
124
+ };
125
+ serviceTags: {
126
+ /** Read the project's / workspace's / label memberships of a service. */
127
+ get: (serviceId: number) => Promise<ServiceTags>;
128
+ /** Replace the membership in a single round-trip. Empty arrays clear. */
129
+ set: (serviceId: number, input: SetServiceTagsInput) => Promise<ServiceTags>;
130
+ };
131
+ projects: {
132
+ /** `query` is appended verbatim, e.g. `?workspaceId=2` (workspace scoping). */
133
+ list: (query?: string) => Promise<ProjectEntry[]>;
134
+ create: (input: CreateProjectInput) => Promise<ProjectEntry>;
135
+ update: (id: number, input: ProjectPatchInput) => Promise<ProjectEntry>;
136
+ remove: (id: number) => Promise<{ ok: boolean }>;
137
+ };
138
+ fanout: {
139
+ /** The extra nodes an image/Dockerfile release is pushed to. */
140
+ get: (serviceId: number) =>
141
+ Promise<Array<{ serverId: number; runtimeId: string | null; status: string }>>;
142
+ /** Replace the target-node set (operator-only). */
143
+ set: (serviceId: number, serverIds: number[]) =>
144
+ Promise<{ targets: Array<{ serverId: number; runtimeId: string | null; status: string }> }>;
145
+ };
146
+ limits: {
147
+ setService: (serviceId: number, input: SetLimitsInput) => Promise<{ cpuShares: number; cpuLimitMilli: number; memLimitMb: number; liveApplied: boolean }>;
148
+ setDatabase: (databaseId: number, input: SetLimitsInput) => Promise<{ cpuShares: number; cpuLimitMilli: number; memLimitMb: number }>;
149
+ };
150
+ attachments: {
151
+ list: (serviceId: number) => Promise<Attachment[]>;
152
+ create: (serviceId: number, input: { databaseId: number; envAlias?: string; reuseExisting?: boolean }) => Promise<Attachment>;
153
+ remove: (serviceId: number, attachmentId: number) => Promise<void>;
154
+ };
155
+ }
156
+
157
+ export function servicesApi(t: Transport): ServicesApi {
158
+ const { get, send, request } = t;
159
+ return {
160
+ services: {
161
+ list: (query) => get<Service[]>(`/v1/services${query ?? ''}`),
162
+ get: (id) => get<ServiceDetail>(`/v1/services/${id}`),
163
+ create: (input) => send<Service>('POST', '/v1/services', input),
164
+ update: (id, input) => send<Service>('PATCH', `/v1/services/${id}`, input),
165
+ composePreview: (input) => send<ComposePreviewResponse>('POST', '/v1/services/compose/preview', input),
166
+ remove: async (id) => {
167
+ await request(`/v1/services/${id}`, { method: 'DELETE' });
168
+ },
169
+ stop: (id) => send<{ ok: boolean; status: string }>('POST', `/v1/services/${id}/stop`),
170
+ start: (id) => send<{ ok: boolean; status: string }>('POST', `/v1/services/${id}/start`),
171
+ restart: (id) => send<{ ok: boolean; status: string }>('POST', `/v1/services/${id}/restart`),
172
+ logs: (id) => get<{ lines: string }>(`/v1/services/${id}/logs`),
173
+ clone: (id, input) => send<Service>('POST', `/v1/services/${id}/clone`, input ?? {}),
174
+ exportUrl: (id) => `/v1/services/${id}/export`,
175
+ importBundle: (bundle) => send<{ ok: boolean; serviceId: number; slug: string; message: string }>('POST', '/v1/services/import', bundle),
176
+ manifest: {
177
+ apply: (serviceId, input) =>
178
+ send<ApplyManifestResult>('POST', `/v1/services/${serviceId}/manifest/apply`, input),
179
+ },
180
+ github: {
181
+ get: (id) => get<ServiceGithubStatus>(`/v1/services/${id}/github`),
182
+ link: (id, input) => send<{ link: ServiceGithubLink }>('PUT', `/v1/services/${id}/github`, input),
183
+ feedback: (id, input) => send<{ link: ServiceGithubLink }>('PATCH', `/v1/services/${id}/github/feedback`, input),
184
+ migrate: (id, sourceId) => send<{ link: ServiceGithubLink }>('POST', `/v1/services/${id}/github/migrate`, { sourceId }),
185
+ finalize: (id) => send<{ link: ServiceGithubLink; webhooksDeactivated: number }>('POST', `/v1/services/${id}/github/finalize`),
186
+ unlink: (id) => send<{ ok: boolean; sourceId: number | null; webhooksReactivated: number }>('DELETE', `/v1/services/${id}/github`),
187
+ },
188
+ // ── 0.16 T8 surfaces ──
189
+ placement: {
190
+ get: (id) => get<ServicePlacementView>(`/v1/services/${id}/placement`),
191
+ set: (id, input) => send<ServicePlacementView>('PUT', `/v1/services/${id}/placement`, input),
192
+ },
193
+ imageTransfers: (id, query) => get<ImageTransfer[]>(apiGetUrl(`/v1/services/${id}/image-transfers`, { ...query })),
194
+ swarm: (id) => get<ServiceSwarmStatus>(`/v1/services/${id}/swarm`),
195
+ // ── end 0.16 T8 ──
196
+ },
197
+ labels: {
198
+ list: (query) => get<Label[]>(`/v1/labels${query ?? ''}`),
199
+ create: (input) => send<Label>('POST', '/v1/labels', input),
200
+ update: (id, input) => send<Label>('PATCH', `/v1/labels/${id}`, input),
201
+ remove: (id) => send<{ ok: boolean }>('DELETE', `/v1/labels/${id}`),
202
+ },
203
+ environments: {
204
+ list: () => get<Environment[]>('/v1/environments'),
205
+ create: (input) => send<Environment>('POST', '/v1/environments', input),
206
+ rename: (id, name) => send<Environment>('PATCH', `/v1/environments/${id}`, { name }),
207
+ remove: (id) => send<{ ok: boolean }>('DELETE', `/v1/environments/${id}`),
208
+ },
209
+ serviceTags: {
210
+ get: (id) => get<ServiceTags>(`/v1/services/${id}/tags`),
211
+ set: (id, input) => send<ServiceTags>('PUT', `/v1/services/${id}/tags`, input),
212
+ },
213
+ projects: {
214
+ list: (query) => get<ProjectEntry[]>(`/v1/projects${query ?? ''}`),
215
+ create: (input) => send<ProjectEntry>('POST', '/v1/projects', input),
216
+ update: (id, input) => send<ProjectEntry>('PATCH', `/v1/projects/${id}`, input),
217
+ remove: (id) => send<{ ok: boolean }>('DELETE', `/v1/projects/${id}`),
218
+ },
219
+ fanout: {
220
+ // r201: the route answers `{ targets: [...] }` (like PATCH); the SDK
221
+ // typed and returned it as a bare array, so the panel's fan-out card
222
+ // called `.map` on an object and crashed whenever it rendered.
223
+ get: async (serviceId) =>
224
+ (
225
+ await get<{ targets: Array<{ serverId: number; runtimeId: string | null; status: string }> }>(
226
+ `/v1/services/${serviceId}/targets`,
227
+ )
228
+ ).targets,
229
+ set: (serviceId, serverIds) =>
230
+ send<{ targets: Array<{ serverId: number; runtimeId: string | null; status: string }> }>(
231
+ 'PATCH',
232
+ `/v1/services/${serviceId}/targets`,
233
+ { serverIds },
234
+ ),
235
+ },
236
+ limits: {
237
+ setService: (serviceId, input) =>
238
+ send<{ cpuShares: number; cpuLimitMilli: number; memLimitMb: number; liveApplied: boolean }>('PATCH', `/v1/services/${serviceId}/limits`, input),
239
+ setDatabase: (databaseId, input) =>
240
+ send<{ cpuShares: number; cpuLimitMilli: number; memLimitMb: number }>('PATCH', `/v1/databases/${databaseId}/limits`, input),
241
+ },
242
+ attachments: {
243
+ list: (serviceId) => get<Attachment[]>(`/v1/services/${serviceId}/attachments`),
244
+ create: (serviceId, input) => send<Attachment>('POST', `/v1/services/${serviceId}/attachments`, input),
245
+ remove: async (serviceId, attachmentId) => {
246
+ await request(`/v1/services/${serviceId}/attachments/${attachmentId}`, { method: 'DELETE' });
247
+ },
248
+ },
249
+ };
250
+ }