@thingd/cli 0.40.2 → 0.41.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.
@@ -1 +1 @@
1
- {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/commands/cloud.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,aAAa,CAAC;AAiBvF,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BjE"}
1
+ {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/commands/cloud.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAA6B,MAAM,aAAa,CAAC;AA6DzE,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BjE"}
@@ -1,7 +1,43 @@
1
+ import { execSync } from "node:child_process";
2
+ import { setTimeout as sleep } from "node:timers/promises";
1
3
  import pc from "picocolors";
2
- import { requiredFlag, requiredToken, stringFlag } from "../index.js";
3
- import { CloudApiError, createApiKey, createInstance, createProject, getMe, listInstances, listProjects, } from "../lib/cloud-api.js";
4
+ import { requiredToken, stringFlag } from "../index.js";
5
+ import { CloudApiError, addOrganizationMember, createApiKey, createInstance, createOrganization, createProject, getMe, getOrganization, listInstances, listOrganizationMembers, listOrganizations, listProjects, pollCliAuth, removeOrganizationMember, startCliAuth, } from "../lib/cloud-api.js";
4
6
  import { readCloudConfig, removeCloudConfig, writeCloudConfig, } from "../lib/cloud-config.js";
7
+ const POLL_INTERVAL_MS = 2_000;
8
+ const POLL_TIMEOUT_MS = 5 * 60 * 1_000;
9
+ function cliApiUrl(context) {
10
+ return stringFlag(context.parsed, "url") ?? process.env.THINGD_URL ?? "https://api.thingd.cloud";
11
+ }
12
+ function buildWebUrl(apiUrl) {
13
+ if (apiUrl.includes("//api.") || apiUrl.startsWith("api.")) {
14
+ return apiUrl.replace("api.", "");
15
+ }
16
+ if (apiUrl.includes("localhost:8787") || apiUrl.includes("127.0.0.1:8787")) {
17
+ return "http://localhost:5173";
18
+ }
19
+ return apiUrl;
20
+ }
21
+ function openBrowser(url) {
22
+ try {
23
+ const platform = process.platform;
24
+ if (platform === "darwin") {
25
+ execSync(`open "${url}"`, { timeout: 5_000 });
26
+ }
27
+ else if (platform === "win32") {
28
+ execSync(`start "" "${url}"`, { timeout: 5_000 });
29
+ }
30
+ else {
31
+ execSync(`xdg-open "${url}"`, { timeout: 5_000 });
32
+ }
33
+ }
34
+ catch {
35
+ // browser open is best-effort
36
+ }
37
+ }
38
+ function makeBaseConfig(context) {
39
+ return { token: "", url: cliApiUrl(context) };
40
+ }
5
41
  export async function runCloud(context) {
6
42
  const sub = requiredToken(context.parsed, 1, "subcommand");
7
43
  switch (sub) {
@@ -23,36 +59,84 @@ export async function runCloud(context) {
23
59
  case "api-key":
24
60
  await runApiKey(context);
25
61
  return;
62
+ case "org":
63
+ await runOrg(context);
64
+ return;
26
65
  default:
27
66
  context.stderr.write(`Unknown cloud subcommand: ${sub}\n` +
28
- "Available: login, logout, status, project, instance, api-key\n");
67
+ "Available: login, logout, status, org, project, instance, api-key\n");
29
68
  }
30
69
  }
31
70
  async function runLogin(context) {
32
- const code = context.parsed.tokens[2] ?? requiredFlag(context.parsed, "code");
71
+ const code = context.parsed.tokens[2] ?? stringFlag(context.parsed, "code");
33
72
  const token = context.parsed.tokens[3] ?? stringFlag(context.parsed, "token");
34
- if (!token) {
35
- context.stdout.write(`First, open this URL in your browser:\n\n` +
36
- ` ${pc.cyan(`https://thingd.cloud/cli/auth?code=${code}`)}\n\n` +
37
- `Then paste the token shown after logging in:\n\n` +
38
- ` ${pc.dim("$ thingd cloud login --code <code> --token <token>")}\n`);
73
+ // ── Manual --code --token flow (fallback) ─────────────────────────
74
+ if (code && token) {
75
+ const config = { token, url: cliApiUrl(context) };
76
+ try {
77
+ const { user } = await getMe(config);
78
+ writeCloudConfig({ ...config, email: user.email });
79
+ context.stdout.write(pc.green(`✓ Logged in as ${user.email}\n`));
80
+ }
81
+ catch (err) {
82
+ if (err instanceof CloudApiError && err.status === 401) {
83
+ context.stderr.write(pc.red("Invalid token. Please try again.\n"));
84
+ }
85
+ else {
86
+ context.stderr.write(pc.red(`Failed to verify token: ${err}\n`));
87
+ }
88
+ }
39
89
  return;
40
90
  }
41
- // Verify token by calling /api/users/me
42
- const config = { token };
91
+ // ── Auto device-code flow ─────────────────────────────────────────
92
+ const baseConfig = makeBaseConfig(context);
93
+ let deviceCode;
43
94
  try {
44
- const { user } = await getMe(config);
45
- writeCloudConfig({ ...config, email: user.email });
46
- context.stdout.write(pc.green(`✓ Logged in as ${user.email}\n`));
95
+ const result = await startCliAuth(baseConfig);
96
+ deviceCode = result.code;
47
97
  }
48
98
  catch (err) {
49
- if (err instanceof CloudApiError && err.status === 401) {
50
- context.stderr.write(pc.red("Invalid token. Please try again.\n"));
99
+ context.stderr.write(pc.red(`Failed to start CLI auth: ${err}\n`));
100
+ return;
101
+ }
102
+ const webUrl = buildWebUrl(cliApiUrl(context));
103
+ const authUrl = `${webUrl}/cli/auth?code=${deviceCode}`;
104
+ context.stdout.write(`\n ${pc.cyan("Opening browser...")}\n` +
105
+ ` ${pc.dim("If the browser doesn't open, visit:")}\n` +
106
+ ` ${pc.dim(authUrl)}\n\n`);
107
+ openBrowser(authUrl);
108
+ // Poll for token
109
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
110
+ let dots = 0;
111
+ while (Date.now() < deadline) {
112
+ await sleep(POLL_INTERVAL_MS);
113
+ try {
114
+ const result = await pollCliAuth(baseConfig, deviceCode);
115
+ if ("token" in result) {
116
+ const tokenConfig = { token: result.token, url: cliApiUrl(context) };
117
+ try {
118
+ const { user } = await getMe(tokenConfig);
119
+ writeCloudConfig({ ...tokenConfig, email: user.email });
120
+ context.stdout.write(pc.green(`\r✓ Logged in as ${user.email}\n`));
121
+ return;
122
+ }
123
+ catch {
124
+ context.stderr.write(pc.red("\rToken received but verification failed. Try again.\n"));
125
+ return;
126
+ }
127
+ }
128
+ // Show spinner
129
+ dots = (dots + 1) % 4;
130
+ context.stdout.write(`\r ${pc.dim(`Waiting for browser login${".".repeat(dots)}${" ".repeat(3 - dots)}`)}`);
51
131
  }
52
- else {
53
- context.stderr.write(pc.red(`Failed to verify token: ${err}\n`));
132
+ catch (err) {
133
+ if (err instanceof CloudApiError && err.status === 410) {
134
+ context.stderr.write(pc.red("\nCode expired. Run `thingd cloud login` again.\n"));
135
+ return;
136
+ }
54
137
  }
55
138
  }
139
+ context.stderr.write(pc.red("\nTimed out. Run `thingd cloud login` again.\n"));
56
140
  }
57
141
  async function runLogout(context) {
58
142
  removeCloudConfig();
@@ -68,11 +152,109 @@ async function runCloudStatus(context) {
68
152
  const { user } = await getMe(config);
69
153
  context.stdout.write(`Logged in as ${pc.green(user.email)} (${user.role})\n` +
70
154
  `API: ${config.url ?? "https://api.thingd.cloud"}\n`);
155
+ if (config.organizationId) {
156
+ try {
157
+ const { organization, role } = await getOrganization(config, config.organizationId);
158
+ context.stdout.write(`Org: ${pc.cyan(organization.name)} (${pc.dim(organization.slug)}) — ${role}\n`);
159
+ }
160
+ catch {
161
+ context.stdout.write(`Org: ${pc.dim("unreachable")}\n`);
162
+ }
163
+ }
164
+ else {
165
+ context.stdout.write(`Org: ${pc.dim("none — set with thingd cloud org use <slug>")}\n`);
166
+ }
71
167
  }
72
168
  catch {
73
169
  context.stdout.write(`Token expired. Run ${pc.cyan("thingd cloud login")}\n`);
74
170
  }
75
171
  }
172
+ // ── Org helpers ──────────────────────────────────────────────────────
173
+ async function resolveOrg(config, slugOrId) {
174
+ // Try direct ID lookup first, then assume it's a slug and list
175
+ try {
176
+ const { organization } = await getOrganization(config, slugOrId);
177
+ return organization;
178
+ }
179
+ catch {
180
+ // Fall through to slug resolution
181
+ }
182
+ const { organizations } = await listOrganizations(config);
183
+ const found = organizations.find((o) => o.slug === slugOrId);
184
+ if (!found) {
185
+ throw new Error(`Organization not found: ${slugOrId}`);
186
+ }
187
+ return found;
188
+ }
189
+ async function runOrg(context) {
190
+ const config = await requireConfig(context);
191
+ const action = context.parsed.tokens[2];
192
+ if (!action || action === "list") {
193
+ const { organizations } = await listOrganizations(config);
194
+ if (organizations.length === 0) {
195
+ context.stdout.write("No organizations found.\n");
196
+ return;
197
+ }
198
+ for (const org of organizations) {
199
+ const active = org.id === config.organizationId ? pc.green(" ●") : "";
200
+ context.stdout.write(`${pc.cyan(org.slug)} ${org.name}${active}\n`);
201
+ }
202
+ return;
203
+ }
204
+ if (action === "create") {
205
+ const name = requiredToken(context.parsed, 3, "name");
206
+ const { organization } = await createOrganization(config, name);
207
+ context.stdout.write(pc.green(`✓ Created organization: ${organization.slug}\n`));
208
+ return;
209
+ }
210
+ if (action === "use") {
211
+ const slugOrId = requiredToken(context.parsed, 3, "organization");
212
+ const org = await resolveOrg(config, slugOrId);
213
+ config.organizationId = org.id;
214
+ writeCloudConfig(config);
215
+ context.stdout.write(pc.green(`✓ Active organization: ${org.slug}\n`));
216
+ return;
217
+ }
218
+ if (action === "members") {
219
+ await runOrgMembers(context, config);
220
+ return;
221
+ }
222
+ context.stderr.write(`Unknown org action: ${action}. Available: list, create, use, members\n`);
223
+ }
224
+ async function runOrgMembers(context, config) {
225
+ const sub = context.parsed.tokens[3];
226
+ if (!sub || sub === "list") {
227
+ const orgSlug = requiredToken(context.parsed, 4, "organization");
228
+ const org = await resolveOrg(config, orgSlug);
229
+ const { members } = await listOrganizationMembers(config, org.id);
230
+ if (members.length === 0) {
231
+ context.stdout.write("No members found.\n");
232
+ return;
233
+ }
234
+ for (const m of members) {
235
+ context.stdout.write(`${pc.cyan(m.userId)} ${m.role}\n`);
236
+ }
237
+ return;
238
+ }
239
+ if (sub === "add") {
240
+ const orgSlug = requiredToken(context.parsed, 4, "organization");
241
+ const userId = requiredToken(context.parsed, 5, "user-id");
242
+ const role = stringFlag(context.parsed, "role") ?? "member";
243
+ const org = await resolveOrg(config, orgSlug);
244
+ const { member } = await addOrganizationMember(config, org.id, userId, role);
245
+ context.stdout.write(pc.green(`✓ Added ${member.userId} as ${member.role}\n`));
246
+ return;
247
+ }
248
+ if (sub === "remove") {
249
+ const orgSlug = requiredToken(context.parsed, 4, "organization");
250
+ const userId = requiredToken(context.parsed, 5, "user-id");
251
+ const org = await resolveOrg(config, orgSlug);
252
+ await removeOrganizationMember(config, org.id, userId);
253
+ context.stdout.write(pc.green(`✓ Removed ${userId}\n`));
254
+ return;
255
+ }
256
+ context.stderr.write(`Unknown members action: ${sub}. Available: list, add, remove\n`);
257
+ }
76
258
  async function requireConfig(context) {
77
259
  const config = readCloudConfig();
78
260
  if (!config) {
@@ -97,8 +279,16 @@ async function runProject(context) {
97
279
  }
98
280
  if (action === "create") {
99
281
  const name = requiredToken(context.parsed, 3, "name");
100
- const { project } = await createProject(config, name);
101
- context.stdout.write(pc.green(`✓ Created project: ${project.slug}\n`));
282
+ // Optional --org flag for team projects
283
+ let organizationId;
284
+ const orgSlug = stringFlag(context.parsed, "org");
285
+ if (orgSlug) {
286
+ const org = await resolveOrg(config, orgSlug);
287
+ organizationId = org.id;
288
+ }
289
+ const { project } = await createProject(config, name, organizationId);
290
+ const ctxNote = organizationId ? ` (under org ${orgSlug})` : "";
291
+ context.stdout.write(pc.green(`✓ Created project: ${project.slug}${ctxNote}\n`));
102
292
  return;
103
293
  }
104
294
  context.stderr.write(`Unknown project action: ${action}. Available: list, create\n`);
@@ -108,7 +298,6 @@ async function runInstance(context) {
108
298
  const action = requiredToken(context.parsed, 2, "action");
109
299
  if (action === "list") {
110
300
  const projectSlug = requiredToken(context.parsed, 3, "project");
111
- // Resolve project slug to ID by listing all projects
112
301
  const { projects } = await listProjects(config);
113
302
  const project = projects.find((p) => p.slug === projectSlug || p.id === projectSlug);
114
303
  if (!project) {
@@ -19,6 +19,20 @@ export type CloudApiKey = {
19
19
  token?: string;
20
20
  createdAt: string;
21
21
  };
22
+ export type CloudOrganization = {
23
+ id: string;
24
+ name: string;
25
+ slug: string;
26
+ createdAt: string;
27
+ };
28
+ export type CloudOrganizationMember = {
29
+ id: string;
30
+ organizationId: string;
31
+ userId: string;
32
+ role: string;
33
+ invitedBy: string;
34
+ joinedAt: string;
35
+ };
22
36
  export declare class CloudApiError extends Error {
23
37
  status: number;
24
38
  constructor(status: number, message: string);
@@ -34,7 +48,7 @@ export declare function getMe(config: CloudConfig): Promise<{
34
48
  export declare function listProjects(config: CloudConfig): Promise<{
35
49
  projects: CloudProject[];
36
50
  }>;
37
- export declare function createProject(config: CloudConfig, name: string): Promise<{
51
+ export declare function createProject(config: CloudConfig, name: string, organizationId?: string): Promise<{
38
52
  project: CloudProject;
39
53
  }>;
40
54
  export declare function listInstances(config: CloudConfig, projectId: string): Promise<{
@@ -46,4 +60,31 @@ export declare function createInstance(config: CloudConfig, projectId: string, n
46
60
  export declare function createApiKey(config: CloudConfig, projectId: string, name?: string): Promise<{
47
61
  key: CloudApiKey;
48
62
  }>;
63
+ export declare function createOrganization(config: CloudConfig, name: string): Promise<{
64
+ organization: CloudOrganization;
65
+ }>;
66
+ export declare function listOrganizations(config: CloudConfig): Promise<{
67
+ organizations: CloudOrganization[];
68
+ }>;
69
+ export declare function getOrganization(config: CloudConfig, orgId: string): Promise<{
70
+ organization: CloudOrganization;
71
+ role: string;
72
+ }>;
73
+ export declare function listOrganizationMembers(config: CloudConfig, orgId: string): Promise<{
74
+ members: CloudOrganizationMember[];
75
+ }>;
76
+ export declare function addOrganizationMember(config: CloudConfig, orgId: string, userId: string, role?: string): Promise<{
77
+ member: CloudOrganizationMember;
78
+ }>;
79
+ export declare function removeOrganizationMember(config: CloudConfig, orgId: string, userId: string): Promise<{
80
+ ok: boolean;
81
+ }>;
82
+ export declare function startCliAuth(config: CloudConfig): Promise<{
83
+ code: string;
84
+ }>;
85
+ export declare function pollCliAuth(config: CloudConfig, code: string): Promise<{
86
+ token: string;
87
+ } | {
88
+ status: string;
89
+ }>;
49
90
  //# sourceMappingURL=cloud-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-api.d.ts","sourceRoot":"","sources":["../../src/lib/cloud-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AASrD,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,qBAAa,aAAc,SAAQ,KAAK;IACtC,MAAM,EAAE,MAAM,CAAC;gBAEH,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK5C;AA0BD,wBAAsB,KAAK,CACzB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC;IAAE,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,CAE9E;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAAC,CAE7F;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,OAAO,EAAE,YAAY,CAAA;CAAE,CAAC,CAEpC;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,SAAS,EAAE,aAAa,EAAE,CAAA;CAAE,CAAC,CAEzC;AAED,wBAAsB,cAAc,CAClC,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,QAAQ,EAAE,aAAa,CAAA;CAAE,CAAC,CAKtC;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC;IAAE,GAAG,EAAE,WAAW,CAAA;CAAE,CAAC,CAK/B"}
1
+ {"version":3,"file":"cloud-api.d.ts","sourceRoot":"","sources":["../../src/lib/cloud-api.ts"],"names":[],"mappings":"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"}
@@ -33,8 +33,12 @@ export async function getMe(config) {
33
33
  export async function listProjects(config) {
34
34
  return request(config, "/api/projects");
35
35
  }
36
- export async function createProject(config, name) {
37
- return request(config, "/api/projects", { method: "POST", body: { name } });
36
+ export async function createProject(config, name, organizationId) {
37
+ const body = { name };
38
+ if (organizationId) {
39
+ body.organizationId = organizationId;
40
+ }
41
+ return request(config, "/api/projects", { method: "POST", body });
38
42
  }
39
43
  export async function listInstances(config, projectId) {
40
44
  return request(config, `/api/projects/${projectId}/instances`);
@@ -51,3 +55,47 @@ export async function createApiKey(config, projectId, name) {
51
55
  body: { name: name ?? "CLI key" },
52
56
  });
53
57
  }
58
+ // ── Organization API ─────────────────────────────────────────────────
59
+ export async function createOrganization(config, name) {
60
+ return request(config, "/api/organizations", { method: "POST", body: { name } });
61
+ }
62
+ export async function listOrganizations(config) {
63
+ return request(config, "/api/organizations");
64
+ }
65
+ export async function getOrganization(config, orgId) {
66
+ return request(config, `/api/organizations/${orgId}`);
67
+ }
68
+ export async function listOrganizationMembers(config, orgId) {
69
+ return request(config, `/api/organizations/${orgId}/members`);
70
+ }
71
+ export async function addOrganizationMember(config, orgId, userId, role = "member") {
72
+ return request(config, `/api/organizations/${orgId}/members`, {
73
+ method: "POST",
74
+ body: { userId, role },
75
+ });
76
+ }
77
+ export async function removeOrganizationMember(config, orgId, userId) {
78
+ return request(config, `/api/organizations/${orgId}/members/${userId}`, {
79
+ method: "DELETE",
80
+ });
81
+ }
82
+ // ── CLI device code auth (unauthenticated) ──────────────────────────
83
+ async function requestUnauthenticated(apiUrl, path, body) {
84
+ const url = `${apiUrl}${path}`;
85
+ const res = await fetch(url, {
86
+ method: "POST",
87
+ headers: { "content-type": "application/json" },
88
+ body: JSON.stringify(body),
89
+ });
90
+ if (!res.ok) {
91
+ const errBody = await res.json().catch(() => ({ message: res.statusText }));
92
+ throw new CloudApiError(res.status, errBody.message ?? res.statusText);
93
+ }
94
+ return res.json();
95
+ }
96
+ export async function startCliAuth(config) {
97
+ return requestUnauthenticated(config.url ?? DEFAULT_API_URL, "/api/auth/cli/start", {});
98
+ }
99
+ export async function pollCliAuth(config, code) {
100
+ return requestUnauthenticated(config.url ?? DEFAULT_API_URL, "/api/auth/cli/poll", { code });
101
+ }
@@ -2,6 +2,8 @@ export type CloudConfig = {
2
2
  token: string;
3
3
  email?: string;
4
4
  url?: string;
5
+ /** Currently active organization context (set by `thingd cloud org use`). */
6
+ organizationId?: string;
5
7
  };
6
8
  export declare function cloudConfigPath(): string;
7
9
  export declare function readCloudConfig(): CloudConfig | null;
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-config.d.ts","sourceRoot":"","sources":["../../src/lib/cloud-config.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,eAAe,IAAI,WAAW,GAAG,IAAI,CAUpD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAG1D;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAMxC"}
1
+ {"version":3,"file":"cloud-config.d.ts","sourceRoot":"","sources":["../../src/lib/cloud-config.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,eAAe,IAAI,WAAW,GAAG,IAAI,CAUpD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAG1D;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAMxC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thingd/cli",
3
- "version": "0.40.2",
3
+ "version": "0.41.0",
4
4
  "description": "CLI, Interactive TUI Dashboard, and MCP server for thingd — a fast object-first data engine for applications and AI agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://engine.thingd.cloud",
@@ -45,7 +45,7 @@
45
45
  "cli-table3": "^0.6.5",
46
46
  "picocolors": "^1.1.1",
47
47
  "zod": "^4.4.3",
48
- "@thingd/sdk": "0.40.2"
48
+ "@thingd/sdk": "0.41.0"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=24.0.0"