@topolo/cli 0.6.8 → 0.7.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.
package/README.md CHANGED
@@ -12,7 +12,7 @@ via `stdin`/`stdout`.
12
12
  - First-class `production`, `staging`, `development`, and `local` environments with separate stored credentials and service URL overrides.
13
13
  - **Per-context credentials** — hold and switch between your personal space and multiple organizations with `topolo context`.
14
14
  - **One-command agent setup** — `topolo skills install` wires the skill + MCP into Claude Code / Codex.
15
- - Commands: `auth`, `whoami`, `context`, `skills`, `apps`, `services`, credential-scoped `actions`, generic `api` passthrough, and internal `platform` scaffolding.
15
+ - Commands: `auth`, `whoami`, `context`, `skills`, `apps`, `services`, `workspaces`, credential-scoped `actions`, generic `api` passthrough, and internal `platform` scaffolding.
16
16
 
17
17
  ## Install
18
18
 
@@ -177,6 +177,22 @@ Both resource flags are required together. This supports concurrent agent
177
177
  operations across many app-local workspaces without changing the CLI/MCP org
178
178
  context or the user's web switcher.
179
179
 
180
+ ### `topolo workspaces`
181
+
182
+ Discovers canonical workspaces and the caller-specific creation policy for a
183
+ target application. Auth owns workspace identity, while its runtime policy
184
+ decides access separately for each app and caller.
185
+
186
+ ```bash
187
+ topolo workspaces list design --json
188
+ topolo workspaces create design --name "Campaigns" --confirm --json
189
+ topolo workspaces rename design <workspace-id> --name "Social campaigns" --confirm --json
190
+ topolo workspaces default design <workspace-id> --confirm --json
191
+ ```
192
+
193
+ Always read `data.policy.canCreate` and `data.policy.reason` from `list` before
194
+ creation. Organization ownership is not treated as permission for every app.
195
+
180
196
  ### `topolo apps`
181
197
 
182
198
  Lists launchable Topolo applications available to the current credential. This
package/dist/index.js CHANGED
@@ -110,57 +110,8 @@ var TopoloHttpError = class extends TopoloSdkError {
110
110
  }
111
111
  };
112
112
  var APP_API_URLS = {
113
- agent: "https://agent.topolo.app",
114
113
  auth: "https://auth.topolo.app",
115
- backup: "https://backup.topolo.app",
116
- blog: "https://blog.topolo.app",
117
- books: "https://books.topolo.app",
118
- brand: "https://brand.topolo.app",
119
- bugfix: "https://bugfix.topolo.app",
120
- bytes: "https://bytes.topolo.app",
121
- calendar: "https://calendar.topolo.app",
122
- campaigns: "https://campaigns.topolo.app",
123
- capacity: "https://capacity.topolo.app",
124
- chat: "https://chat.topolo.app",
125
- commerce: "https://commerce.topolo.app",
126
- compose: "https://compose.topolo.app",
127
- consent: "https://consent.topolo.app",
128
- crm: "https://crm.topolo.app",
129
- design: "https://design.topolo.app",
130
- developers: "https://developers.topolo.app",
131
- director: "https://director.topolo.app",
132
- feed: "https://feed.topolo.app",
133
- flow: "https://flow.topolo.app",
134
- forecast: "https://forecast.topolo.app",
135
- forms: "https://forms.topolo.app",
136
- home: "https://home.topolo.app",
137
- insights: "https://insights.topolo.app",
138
- inventory: "https://inventory.topolo.app",
139
- learn: "https://learn.topolo.app",
140
- localize: "https://localize.topolo.app",
141
- mail: "https://mail.topolo.app",
142
- mdm: "https://mdm.topolo.app",
143
- messages: "https://messages.topolo.app",
144
- nexus: "https://nexus.topolo.app",
145
- notify: "https://notify.topolo.app",
146
- observability_watch: "https://observability-watch.topolo.app",
147
- one: "https://www.topolo.app",
148
- p2p: "https://p2p.topolo.app",
149
- pay: "https://pay.topolo.app",
150
- people: "https://people.topolo.app",
151
- "quro.api": "https://api.ol0.me",
152
- "quro.redirect": "https://ol0.me",
153
- roadmapper: "https://roadmapper.topolo.app",
154
- sign: "https://sign.topolo.app",
155
- social_studio: "https://studio.topolo.app",
156
- socialize: "https://socialize.topolo.app",
157
- spaces: "https://spaces.topolo.app",
158
- status: "https://status.topolo.app",
159
- success: "https://success.topolo.app",
160
- support: "https://support.topolo.app",
161
- survey: "https://survey.topolo.app",
162
- voice: "https://voice.topolo.app",
163
- web: "https://web.topolo.app"
114
+ developers: "https://developers.topolo.app"
164
115
  };
165
116
  var APPLICATIONS = {
166
117
  "admin": {
@@ -1886,7 +1837,145 @@ var APPLICATIONS = {
1886
1837
  ]
1887
1838
  }
1888
1839
  };
1889
- var APPLICATION_REQUIREMENTS_VERSION = "2026-06-07.1";
1840
+ function friendlyAppId(entry) {
1841
+ for (const value of [entry.slug, entry.name, entry.appId]) {
1842
+ if (typeof value !== "string" || !value.trim()) continue;
1843
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
1844
+ const withoutPrefix = normalized.replace(/^(?:app[_-])?topolo[-_]+/, "");
1845
+ if (withoutPrefix) return withoutPrefix;
1846
+ }
1847
+ return entry.appId;
1848
+ }
1849
+ function idVariants(value) {
1850
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
1851
+ return [.../* @__PURE__ */ new Set([
1852
+ normalized,
1853
+ normalized.replace(/-/g, "_"),
1854
+ normalized.replace(/_/g, "-")
1855
+ ])].filter(Boolean);
1856
+ }
1857
+ var STATIC_APPLICATIONS = APPLICATIONS;
1858
+ function staticApplicationForId(id) {
1859
+ for (const variant of idVariants(id)) {
1860
+ const entry = STATIC_APPLICATIONS[variant];
1861
+ if (entry) return entry;
1862
+ }
1863
+ return null;
1864
+ }
1865
+ function applicationEntryFromCatalog(live) {
1866
+ const id = friendlyAppId(live);
1867
+ return {
1868
+ id,
1869
+ appDir: "-",
1870
+ name: live.name ?? live.slug ?? live.appId,
1871
+ packageName: null,
1872
+ productionUrl: live.launchUrl ?? live.apiBaseUrl ?? null,
1873
+ services: live.apiBaseUrl ? [id] : [],
1874
+ deployTargets: []
1875
+ };
1876
+ }
1877
+ var DIRECTORY_PAGE_LIMIT = 100;
1878
+ var DIRECTORY_MAX_PAGES = 100;
1879
+ function staticApplicationDirectory() {
1880
+ const applications = Object.entries(STATIC_APPLICATIONS).map(([id, application]) => ({
1881
+ id,
1882
+ application,
1883
+ source: "static",
1884
+ live: null
1885
+ })).sort((left, right) => left.id.localeCompare(right.id));
1886
+ return { source: "static_fallback", applications };
1887
+ }
1888
+ function directoryApplicationFromLive(live) {
1889
+ const id = friendlyAppId(live);
1890
+ return {
1891
+ id,
1892
+ // Prefer the static snapshot's richer repo/deploy metadata when the live
1893
+ // registry confirms the app; synthesize from the live entry otherwise.
1894
+ application: staticApplicationForId(id) ?? applicationEntryFromCatalog(live),
1895
+ source: "live",
1896
+ live
1897
+ };
1898
+ }
1899
+ async function listApplicationDirectory(client) {
1900
+ if (!client) return staticApplicationDirectory();
1901
+ let services;
1902
+ try {
1903
+ services = [];
1904
+ let cursor;
1905
+ for (let page = 0; page < DIRECTORY_MAX_PAGES; page += 1) {
1906
+ const result = await client.searchServices({
1907
+ ...cursor ? { cursor } : {},
1908
+ limit: DIRECTORY_PAGE_LIMIT
1909
+ });
1910
+ services.push(...result.services);
1911
+ if (!result.page.nextCursor) break;
1912
+ cursor = result.page.nextCursor;
1913
+ }
1914
+ } catch {
1915
+ return staticApplicationDirectory();
1916
+ }
1917
+ const seen = /* @__PURE__ */ new Map();
1918
+ for (const live of services) {
1919
+ const entry = directoryApplicationFromLive(live);
1920
+ if (!seen.has(entry.id)) seen.set(entry.id, entry);
1921
+ }
1922
+ return {
1923
+ source: "live",
1924
+ applications: [...seen.values()].sort((left, right) => left.id.localeCompare(right.id))
1925
+ };
1926
+ }
1927
+ function matchesDirectoryApplication(app, needle) {
1928
+ const candidates = new Set(idVariants(needle));
1929
+ const keys = [
1930
+ app.id,
1931
+ app.application.id,
1932
+ app.application.name,
1933
+ app.live?.appId,
1934
+ app.live?.slug,
1935
+ app.live?.name
1936
+ ];
1937
+ for (const key of keys) {
1938
+ if (typeof key !== "string" || !key.trim()) continue;
1939
+ for (const variant of idVariants(key)) {
1940
+ if (candidates.has(variant)) return true;
1941
+ }
1942
+ for (const variant of idVariants(friendlyAppId({ appId: key }))) {
1943
+ if (candidates.has(variant)) return true;
1944
+ }
1945
+ }
1946
+ return false;
1947
+ }
1948
+ function formatDirectoryApplicationIds(directory) {
1949
+ const ids = directory.applications.map((app) => app.id).sort();
1950
+ const preview = ids.slice(0, 50).join(", ");
1951
+ const suffix = ids.length > 50 ? `, ... +${ids.length - 50} more` : "";
1952
+ const label = directory.source === "live" ? "Applications available to this credential" : "Applications in the bundled static snapshot (live registry unreachable)";
1953
+ return `${label}: ${preview}${suffix}`;
1954
+ }
1955
+ async function resolveApplicationDirectoryEntry(client, application) {
1956
+ const needle = typeof application === "string" ? application.trim() : "";
1957
+ if (!needle) {
1958
+ throw new TopoloSdkError("unknown_application", "application is required");
1959
+ }
1960
+ if (client) {
1961
+ let live = null;
1962
+ try {
1963
+ live = await client.getService(needle);
1964
+ } catch {
1965
+ live = null;
1966
+ }
1967
+ if (live) return directoryApplicationFromLive(live);
1968
+ }
1969
+ const directory = await listApplicationDirectory(client);
1970
+ const match = directory.applications.find((app) => matchesDirectoryApplication(app, needle));
1971
+ if (match) return match;
1972
+ throw new TopoloSdkError(
1973
+ "unknown_application",
1974
+ `Unknown application "${application}". ${formatDirectoryApplicationIds(directory)}`
1975
+ );
1976
+ }
1977
+ var STATIC_APPLICATIONS2 = APPLICATIONS;
1978
+ var APPLICATION_REQUIREMENTS_VERSION = "2026-08-11.1";
1890
1979
  var APPLICATION_REQUIREMENTS = [
1891
1980
  {
1892
1981
  id: "platform-metadata",
@@ -1896,10 +1985,10 @@ var APPLICATION_REQUIREMENTS = [
1896
1985
  evidence: [
1897
1986
  "topolo.cloudcontrol.json exists for every deployed app surface",
1898
1987
  "package.json names the package or app when the repo ships code",
1899
- "The SDK application catalog exposes the app through topolo apps and MCP discovery"
1988
+ "The live platform registry exposes the app through topolo apps and MCP discovery"
1900
1989
  ],
1901
1990
  implementation: [
1902
- "Add the app to the SDK application catalog generator before exposing it to agents",
1991
+ "Register the app in the live platform registry (Auth service catalog) before exposing it to agents",
1903
1992
  "Keep production URLs, Worker names, Pages projects, routes, domains, and bindings current in CloudControl metadata",
1904
1993
  "Do not leave a new Topolo* app directory without an explicit application ID"
1905
1994
  ]
@@ -1974,7 +2063,7 @@ var APPLICATION_REQUIREMENTS = [
1974
2063
  implementation: [
1975
2064
  "Add production Worker api_url metadata and sdk_app_id for every agent-callable API target",
1976
2065
  "Mark browser, documentation, and runtime-only Worker targets with agent_callable:false",
1977
- "Expose stable app IDs through generated DEFAULT_APP_URLS and live catalog discovery",
2066
+ "Expose stable app IDs through live catalog discovery",
1978
2067
  "Keep typed SDK/CLI/MCP surfaces in sync when an API contract stabilizes"
1979
2068
  ]
1980
2069
  },
@@ -2094,15 +2183,24 @@ function applicationRequirementScopes(application) {
2094
2183
  }
2095
2184
  return Array.from(scopes);
2096
2185
  }
2186
+ function fallbackApplication(id) {
2187
+ const entry = STATIC_APPLICATIONS2[id];
2188
+ if (!entry) {
2189
+ throw new Error(
2190
+ `Unknown application "${id}" in the static fallback snapshot. Resolve applications from the live registry (resolveApplicationDirectoryEntry) and pass the entry directly.`
2191
+ );
2192
+ }
2193
+ return entry;
2194
+ }
2097
2195
  function requirementsForApplication(applicationOrId) {
2098
- const application = typeof applicationOrId === "string" ? APPLICATIONS[applicationOrId] : applicationOrId;
2196
+ const application = typeof applicationOrId === "string" ? fallbackApplication(applicationOrId) : applicationOrId;
2099
2197
  const scopes = new Set(applicationRequirementScopes(application));
2100
2198
  return APPLICATION_REQUIREMENTS.filter(
2101
2199
  (requirement) => requirement.appliesTo.some((scope) => scopes.has(scope))
2102
2200
  );
2103
2201
  }
2104
2202
  function auditApplicationRequirements(applicationOrId) {
2105
- const application = typeof applicationOrId === "string" ? APPLICATIONS[applicationOrId] : applicationOrId;
2203
+ const application = typeof applicationOrId === "string" ? fallbackApplication(applicationOrId) : applicationOrId;
2106
2204
  const scopes = applicationRequirementScopes(application);
2107
2205
  const findings = requirementsForApplication(application).map(
2108
2206
  (requirement) => evaluateRequirement(application, requirement)
@@ -2115,10 +2213,12 @@ function auditApplicationRequirements(applicationOrId) {
2115
2213
  findings
2116
2214
  };
2117
2215
  }
2118
- function auditAllApplicationRequirements(applicationIds = Object.keys(APPLICATIONS).sort()) {
2119
- const applications = applicationIds.map(
2120
- (applicationId) => auditApplicationRequirements(applicationId)
2216
+ function auditApplicationEntries(applications) {
2217
+ return buildAuditReport(
2218
+ applications.map((application) => auditApplicationRequirements(application))
2121
2219
  );
2220
+ }
2221
+ function buildAuditReport(applications) {
2122
2222
  const migrationQueue = applications.flatMap(
2123
2223
  (audit) => audit.findings.filter((finding2) => finding2.status !== "met").map((finding2) => ({
2124
2224
  applicationId: audit.application.id,
@@ -3421,8 +3521,9 @@ var TopoloClient = class _TopoloClient {
3421
3521
  });
3422
3522
  }
3423
3523
  /**
3424
- * Bundled generated registry for tooling that audits platform packaging, not
3425
- * for deciding what the current credential may use.
3524
+ * @deprecated Static fallback snapshot only. Use `searchServices()` (the
3525
+ * live credential-scoped registry) for anything that decides behavior; this
3526
+ * exists for offline packaging audits when the registry is unreachable.
3426
3527
  */
3427
3528
  listBundledServices() {
3428
3529
  const apps = Object.values(APPLICATIONS);
@@ -3735,10 +3836,19 @@ function mapActionInput(action, input3) {
3735
3836
  delete remaining[key];
3736
3837
  return encodeURIComponent(String(value));
3737
3838
  });
3738
- const explicitQuery = plainObject(remaining.query);
3739
- delete remaining.query;
3740
- const explicitBody = Object.prototype.hasOwnProperty.call(remaining, "body") ? remaining.body : void 0;
3741
- delete remaining.body;
3839
+ const schemaProperties = plainObject(plainObject(action.inputSchema).properties);
3840
+ const schemaOwnsQuery = Object.prototype.hasOwnProperty.call(schemaProperties, "query");
3841
+ const schemaOwnsBody = Object.prototype.hasOwnProperty.call(schemaProperties, "body");
3842
+ let explicitQuery = {};
3843
+ if (!schemaOwnsQuery) {
3844
+ explicitQuery = plainObject(remaining.query);
3845
+ delete remaining.query;
3846
+ }
3847
+ let explicitBody;
3848
+ if (!schemaOwnsBody) {
3849
+ explicitBody = Object.prototype.hasOwnProperty.call(remaining, "body") ? remaining.body : void 0;
3850
+ delete remaining.body;
3851
+ }
3742
3852
  if (action.method === "GET") {
3743
3853
  return {
3744
3854
  path: path9,
@@ -3862,6 +3972,65 @@ var IdentityModule = class {
3862
3972
  return this.client.introspect();
3863
3973
  }
3864
3974
  };
3975
+ var ACTIONS = {
3976
+ list: "app_topolo_auth.workspaces.list",
3977
+ create: "app_topolo_auth.workspaces.create",
3978
+ rename: "app_topolo_auth.workspaces.rename",
3979
+ setDefault: "app_topolo_auth.workspaces.default.set"
3980
+ };
3981
+ var WorkspacesModule = class {
3982
+ constructor(client) {
3983
+ this.client = client;
3984
+ }
3985
+ client;
3986
+ async list(application) {
3987
+ const context = await this.resolveContext(application);
3988
+ return this.client.callAction(ACTIONS.list, {
3989
+ org_id: context.organizationId,
3990
+ app_id: context.appId
3991
+ });
3992
+ }
3993
+ async create(application, input3) {
3994
+ const current = await this.list(application);
3995
+ if (!current.data.policy.canCreate) {
3996
+ throw new TopoloSdkError(
3997
+ "workspace_creation_not_allowed",
3998
+ `Workspace creation is not allowed for ${current.data.appId}: ${current.data.policy.reason}`
3999
+ );
4000
+ }
4001
+ return this.client.callAction(ACTIONS.create, {
4002
+ organizationId: current.data.organizationId,
4003
+ appId: current.data.appId,
4004
+ name: input3.name,
4005
+ ...input3.isDefault === void 0 ? {} : { isDefault: input3.isDefault }
4006
+ }, { confirm: input3.confirm === true });
4007
+ }
4008
+ async rename(application, workspaceId, name, confirm = false) {
4009
+ const context = await this.resolveContext(application);
4010
+ return this.client.callAction(ACTIONS.rename, {
4011
+ workspaceId,
4012
+ organizationId: context.organizationId,
4013
+ appId: context.appId,
4014
+ name
4015
+ }, { confirm });
4016
+ }
4017
+ async setDefault(application, workspaceId, confirm = false) {
4018
+ const context = await this.resolveContext(application);
4019
+ return this.client.callAction(ACTIONS.setDefault, {
4020
+ workspaceId,
4021
+ organizationId: context.organizationId,
4022
+ appId: context.appId
4023
+ }, { confirm });
4024
+ }
4025
+ async resolveContext(application) {
4026
+ const identity = await this.client.introspect();
4027
+ if (!identity.organization?.id) {
4028
+ throw new TopoloSdkError("organization_required", "Workspace lifecycle requires an organization-scoped credential.");
4029
+ }
4030
+ const service = await this.client.getService(application);
4031
+ return { organizationId: identity.organization.id, appId: service.appId };
4032
+ }
4033
+ };
3865
4034
  var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
3866
4035
  var TopoloOAuth = class {
3867
4036
  baseUrl;
@@ -3945,6 +4114,7 @@ function createTopolo(options) {
3945
4114
  return {
3946
4115
  client,
3947
4116
  identity: new IdentityModule(client),
4117
+ workspaces: new WorkspacesModule(client),
3948
4118
  crm: new CrmModule(client)
3949
4119
  };
3950
4120
  }
@@ -4010,57 +4180,8 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
4010
4180
  }
4011
4181
  };
4012
4182
  var APP_API_URLS2 = {
4013
- agent: "https://agent.topolo.app",
4014
4183
  auth: "https://auth.topolo.app",
4015
- backup: "https://backup.topolo.app",
4016
- blog: "https://blog.topolo.app",
4017
- books: "https://books.topolo.app",
4018
- brand: "https://brand.topolo.app",
4019
- bugfix: "https://bugfix.topolo.app",
4020
- bytes: "https://bytes.topolo.app",
4021
- calendar: "https://calendar.topolo.app",
4022
- campaigns: "https://campaigns.topolo.app",
4023
- capacity: "https://capacity.topolo.app",
4024
- chat: "https://chat.topolo.app",
4025
- commerce: "https://commerce.topolo.app",
4026
- compose: "https://compose.topolo.app",
4027
- consent: "https://consent.topolo.app",
4028
- crm: "https://crm.topolo.app",
4029
- design: "https://design.topolo.app",
4030
- developers: "https://developers.topolo.app",
4031
- director: "https://director.topolo.app",
4032
- feed: "https://feed.topolo.app",
4033
- flow: "https://flow.topolo.app",
4034
- forecast: "https://forecast.topolo.app",
4035
- forms: "https://forms.topolo.app",
4036
- home: "https://home.topolo.app",
4037
- insights: "https://insights.topolo.app",
4038
- inventory: "https://inventory.topolo.app",
4039
- learn: "https://learn.topolo.app",
4040
- localize: "https://localize.topolo.app",
4041
- mail: "https://mail.topolo.app",
4042
- mdm: "https://mdm.topolo.app",
4043
- messages: "https://messages.topolo.app",
4044
- nexus: "https://nexus.topolo.app",
4045
- notify: "https://notify.topolo.app",
4046
- observability_watch: "https://observability-watch.topolo.app",
4047
- one: "https://www.topolo.app",
4048
- p2p: "https://p2p.topolo.app",
4049
- pay: "https://pay.topolo.app",
4050
- people: "https://people.topolo.app",
4051
- "quro.api": "https://api.ol0.me",
4052
- "quro.redirect": "https://ol0.me",
4053
- roadmapper: "https://roadmapper.topolo.app",
4054
- sign: "https://sign.topolo.app",
4055
- social_studio: "https://studio.topolo.app",
4056
- socialize: "https://socialize.topolo.app",
4057
- spaces: "https://spaces.topolo.app",
4058
- status: "https://status.topolo.app",
4059
- success: "https://success.topolo.app",
4060
- support: "https://support.topolo.app",
4061
- survey: "https://survey.topolo.app",
4062
- voice: "https://voice.topolo.app",
4063
- web: "https://web.topolo.app"
4184
+ developers: "https://developers.topolo.app"
4064
4185
  };
4065
4186
  var DEFAULT_APP_URLS2 = APP_API_URLS2;
4066
4187
  function resolveServiceUrl2(service, overrides) {
@@ -6454,12 +6575,6 @@ function normalizeContainerAppId(value, surfaceModel) {
6454
6575
  function normalizeApplicationId(application) {
6455
6576
  const raw = application.trim().toLowerCase();
6456
6577
  const normalized = raw.replace(/-/g, "_");
6457
- if (Object.prototype.hasOwnProperty.call(APPLICATIONS, raw)) {
6458
- return raw;
6459
- }
6460
- if (Object.prototype.hasOwnProperty.call(APPLICATIONS, normalized)) {
6461
- return normalized;
6462
- }
6463
6578
  if (/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(raw) || /^[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/.test(raw)) {
6464
6579
  return normalized;
6465
6580
  }
@@ -9845,7 +9960,7 @@ function applicationScaffoldPayload(application, options = {}) {
9845
9960
  const requestedId = normalizeApplicationId(application);
9846
9961
  const id = target === "platform" ? normalizeFirstPartyApplicationId(requestedId) : requestedId;
9847
9962
  const slug = toSlug(id);
9848
- const catalogApplication = APPLICATIONS[id];
9963
+ const catalogApplication = options.catalogApplication ?? null;
9849
9964
  const browser = options.browser !== false;
9850
9965
  const api = options.api ?? target === "platform";
9851
9966
  const audience = normalizeAudience(
@@ -11481,7 +11596,6 @@ function registerApplicationReconcileCommand(parent, ownerType, dependencies = {
11481
11596
  }
11482
11597
 
11483
11598
  // src/commands/apps.ts
11484
- var APPLICATION_IDS = Object.keys(APPLICATIONS).sort();
11485
11599
  var CLOUDCONTROL_LOCAL_APPLICATION = {
11486
11600
  id: "cloudcontrol",
11487
11601
  appDir: "TopoloCloudControl",
@@ -11491,6 +11605,21 @@ var CLOUDCONTROL_LOCAL_APPLICATION = {
11491
11605
  services: [],
11492
11606
  deployTargets: []
11493
11607
  };
11608
+ async function directoryClient() {
11609
+ try {
11610
+ const topolo = await buildClient();
11611
+ return topolo ? topolo.client : null;
11612
+ } catch {
11613
+ return null;
11614
+ }
11615
+ }
11616
+ async function resolveScaffoldCatalogApplication(application) {
11617
+ try {
11618
+ return await resolveApplication(application, await directoryClient());
11619
+ } catch {
11620
+ return null;
11621
+ }
11622
+ }
11494
11623
  function registerAppsCommand(program2) {
11495
11624
  const apps = program2.command("apps").description("Search Topolo platform applications available to the current credential").option("-q, --query <query>", "Filter by app id, slug, or name").option("--cursor <cursor>", "Continuation cursor from the previous page").option("--limit <limit>", "Page size (1-100)", "25").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
11496
11625
  const mode = resolveOutputMode(opts.json);
@@ -11528,10 +11657,10 @@ function registerAppsCommand(program2) {
11528
11657
  });
11529
11658
  apps.command("requirements [application]").description(
11530
11659
  "Show the versioned Topolo application-build requirements, optionally filtered to one app."
11531
- ).option("--json", "Output JSON", true).option("--no-json", "Output human-readable details").action((application, opts) => {
11660
+ ).option("--json", "Output JSON", true).option("--no-json", "Output human-readable details").action(async (application, opts) => {
11532
11661
  const mode = resolveOutputMode(opts.json);
11533
11662
  try {
11534
- const payload = applicationRequirementsPayload(application);
11663
+ const payload = await applicationRequirementsPayload(application, await directoryClient());
11535
11664
  emit(payload, mode, () => formatRequirements(payload));
11536
11665
  } catch (err) {
11537
11666
  fail(err, mode);
@@ -11542,12 +11671,13 @@ function registerAppsCommand(program2) {
11542
11671
  ).option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").option("--local", "Inspect the local Topolo checkout for filesystem-backed evidence").option("--repo-root <path>", "Topolo checkout root to inspect when --local is set").option(
11543
11672
  "--fail-on <status>",
11544
11673
  "Exit non-zero when audit has findings at this severity or worse: missing, needs_review, partial"
11545
- ).action((application, opts) => {
11674
+ ).action(async (application, opts) => {
11546
11675
  const mode = resolveOutputMode(opts.json);
11547
11676
  try {
11548
- const payload = applicationAuditPayload(application, {
11677
+ const payload = await applicationAuditPayload(application, {
11549
11678
  local: opts.local,
11550
- repoRoot: opts.repoRoot
11679
+ repoRoot: opts.repoRoot,
11680
+ client: await directoryClient()
11551
11681
  });
11552
11682
  const gate = opts.failOn ? evaluateApplicationAuditGate(payload, opts.failOn) : null;
11553
11683
  const output2 = gate ? { ...payload, conformanceGate: gate } : payload;
@@ -11567,10 +11697,13 @@ function registerAppsCommand(program2) {
11567
11697
  });
11568
11698
  apps.command("record-evidence <application>").description(
11569
11699
  "Stamp <repo>/.topolo/evidence.json with the latest CI gate results so the audit can flip verification-gates / observability-and-audit / canonical-docs from partial to met when the recorded run is fresh (< 24h)."
11570
- ).option("--json", "Output JSON", true).option("--no-json", "Output human-readable summary").option("--app-root <path>", "Path to the application repo root (defaults to cwd)").option("--build", "Mark the latest build as successful").option("--typecheck", "Mark the latest typecheck as successful").option("--test", "Mark the latest test run as successful").option("--docs-validate", "Mark the latest TopoloDocs npm run validate as successful").option("--smoke <url>", "Mark the latest live smoke as successful against this URL").option("--commit <sha>", "Override the commit hash (defaults to git HEAD)").action((application, opts) => {
11700
+ ).option("--json", "Output JSON", true).option("--no-json", "Output human-readable summary").option("--app-root <path>", "Path to the application repo root (defaults to cwd)").option("--build", "Mark the latest build as successful").option("--typecheck", "Mark the latest typecheck as successful").option("--test", "Mark the latest test run as successful").option("--docs-validate", "Mark the latest TopoloDocs npm run validate as successful").option("--smoke <url>", "Mark the latest live smoke as successful against this URL").option("--commit <sha>", "Override the commit hash (defaults to git HEAD)").action(async (application, opts) => {
11571
11701
  const mode = resolveOutputMode(opts.json);
11572
11702
  try {
11573
- const payload = recordApplicationEvidence(application, opts);
11703
+ const payload = await recordApplicationEvidence(application, {
11704
+ ...opts,
11705
+ client: await directoryClient()
11706
+ });
11574
11707
  emit(payload, mode, () => formatEvidence(payload));
11575
11708
  } catch (err) {
11576
11709
  fail(err, mode);
@@ -11592,6 +11725,7 @@ function registerAppsCommand(program2) {
11592
11725
  try {
11593
11726
  const api = resolveScaffoldApiOption(opts.api, opts.withApi);
11594
11727
  const payload = applicationScaffoldPayload(application, {
11728
+ catalogApplication: await resolveScaffoldCatalogApplication(application),
11595
11729
  appDir: opts.appDir,
11596
11730
  name: opts.name,
11597
11731
  packageName: opts.packageName,
@@ -11653,14 +11787,11 @@ function parsePageSize2(value) {
11653
11787
  }
11654
11788
  return parsed;
11655
11789
  }
11656
- function resolveApplication(application) {
11657
- const id = application.trim().toLowerCase();
11658
- if (!APPLICATION_IDS.includes(id)) {
11659
- throw new Error(`Unknown application "${application}". ${formatKnownValues(APPLICATION_IDS, "applications")}`);
11660
- }
11661
- return APPLICATIONS[id];
11790
+ async function resolveApplication(application, client = null) {
11791
+ const resolved = await resolveApplicationDirectoryEntry(client, application);
11792
+ return resolved.application;
11662
11793
  }
11663
- function applicationRequirementsPayload(application) {
11794
+ async function applicationRequirementsPayload(application, client = null) {
11664
11795
  if (!application) {
11665
11796
  return {
11666
11797
  version: APPLICATION_REQUIREMENTS_VERSION,
@@ -11669,7 +11800,7 @@ function applicationRequirementsPayload(application) {
11669
11800
  requirements: [...APPLICATION_REQUIREMENTS]
11670
11801
  };
11671
11802
  }
11672
- const app = resolveApplication(application);
11803
+ const app = await resolveApplication(application, client);
11673
11804
  return {
11674
11805
  version: APPLICATION_REQUIREMENTS_VERSION,
11675
11806
  application: app,
@@ -11677,18 +11808,21 @@ function applicationRequirementsPayload(application) {
11677
11808
  requirements: requirementsForApplication(app)
11678
11809
  };
11679
11810
  }
11680
- function applicationAuditPayload(application, options = {}) {
11811
+ async function applicationAuditPayload(application, options = {}) {
11812
+ const client = options.client ?? null;
11681
11813
  const requestedId = application?.trim().toLowerCase();
11682
11814
  const report = requestedId === "cloudcontrol" && options.local ? {
11683
11815
  version: APPLICATION_REQUIREMENTS_VERSION,
11684
11816
  applications: [auditApplicationRequirements(CLOUDCONTROL_LOCAL_APPLICATION)],
11685
11817
  migrationQueue: []
11686
- } : application ? auditAllApplicationRequirements([resolveApplication(application).id]) : auditAllApplicationRequirements();
11818
+ } : application ? auditApplicationEntries([await resolveApplication(application, client)]) : auditApplicationEntries(
11819
+ (await listApplicationDirectory(client)).applications.map((app) => app.application)
11820
+ );
11687
11821
  if (!options.local) return report;
11688
11822
  return enrichApplicationAuditWithLocalEvidence(report, { repoRoot: options.repoRoot });
11689
11823
  }
11690
- function recordApplicationEvidence(application, options = {}) {
11691
- const appId = resolveApplication(application).id;
11824
+ async function recordApplicationEvidence(application, options = {}) {
11825
+ const appId = (await resolveApplication(application, options.client ?? null)).id;
11692
11826
  const root = path6.resolve(options.appRoot ?? process.cwd());
11693
11827
  if (!existsSync4(root)) {
11694
11828
  throw new Error(`Application root does not exist: ${root}`);
@@ -11819,22 +11953,7 @@ function applicationUrl(application) {
11819
11953
  return application.apiBaseUrl ?? application.launchUrl ?? application.productionUrl ?? null;
11820
11954
  }
11821
11955
  function displayCatalogId(entry) {
11822
- for (const value of [entry.slug, entry.name, entry.appId]) {
11823
- if (typeof value !== "string" || !value.trim()) continue;
11824
- const candidates = catalogIdCandidates(value);
11825
- const withoutTopolo = candidates.find((candidate) => !candidate.startsWith("topolo-"));
11826
- if (withoutTopolo) return withoutTopolo;
11827
- if (candidates[0]) return candidates[0];
11828
- }
11829
- return entry.appId;
11830
- }
11831
- function catalogIdCandidates(value) {
11832
- const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
11833
- const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
11834
- return [.../* @__PURE__ */ new Set([
11835
- normalized,
11836
- withoutTopolo
11837
- ])].filter(Boolean);
11956
+ return friendlyAppId(entry);
11838
11957
  }
11839
11958
  function formatRequirements(payload) {
11840
11959
  const lines = [
@@ -11998,12 +12117,6 @@ function normalizeFailOn(value) {
11998
12117
  }
11999
12118
  throw new Error("Invalid --fail-on value. Use missing, needs_review, or partial.");
12000
12119
  }
12001
- function formatKnownValues(values, label) {
12002
- const sorted = [...values].sort();
12003
- const preview = sorted.slice(0, 25).join(", ");
12004
- const suffix = sorted.length > 25 ? `, ... +${sorted.length - 25} more` : "";
12005
- return `Run discovery for the full ${label} list. Known snapshot sample: ${preview}${suffix}`;
12006
- }
12007
12120
  function statusesAtOrWorse(threshold) {
12008
12121
  if (threshold === "missing") return ["missing"];
12009
12122
  if (threshold === "needs_review") return ["missing", "needs_review"];
@@ -12205,6 +12318,7 @@ function registerPlatformCommand(program2) {
12205
12318
  }
12206
12319
  const api = resolveScaffoldApiOption(opts.api, opts.withApi);
12207
12320
  const payload = applicationScaffoldPayload(application, {
12321
+ catalogApplication: await resolveScaffoldCatalogApplication(application),
12208
12322
  appDir: opts.appDir,
12209
12323
  name: opts.name,
12210
12324
  packageName: opts.packageName,
@@ -12456,15 +12570,7 @@ function isStagingUrl(value) {
12456
12570
  return url.includes(".stg.topolo.us") || url.includes("localhost") || url.includes("127.0.0.1") || url.includes("topolo-staging");
12457
12571
  }
12458
12572
  function displayAppId(entry) {
12459
- for (const value of [entry.slug, entry.name, entry.appId]) {
12460
- if (typeof value !== "string" || !value.trim()) continue;
12461
- for (const candidate of appIdCandidates(value)) {
12462
- if (Object.prototype.hasOwnProperty.call(DEFAULT_APP_URLS, candidate)) {
12463
- return candidate;
12464
- }
12465
- }
12466
- }
12467
- return appIdCandidates(entry.slug || entry.appId)[0] || entry.appId;
12573
+ return friendlyAppId(entry);
12468
12574
  }
12469
12575
  function defaultServiceUrl(id, entry) {
12470
12576
  return resolveCatalogServiceUrl(entry, { serviceKey: id });
@@ -13461,6 +13567,79 @@ function formatResources(result) {
13461
13567
  return lines.join("\n");
13462
13568
  }
13463
13569
 
13570
+ // src/commands/workspaces.ts
13571
+ function registerWorkspacesCommand(program2) {
13572
+ const workspaces = program2.command("workspaces").description("Discover and manage caller-authorized workspaces for one Topolo application");
13573
+ workspaces.command("list <application>").description("List workspaces and the caller-specific creation policy for one application").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (application, opts) => {
13574
+ const mode = resolveOutputMode(opts.json);
13575
+ try {
13576
+ const topolo = await requiredClient2();
13577
+ const result = await topolo.workspaces.list(application);
13578
+ emit(result, mode, () => formatWorkspaceList(result));
13579
+ } catch (error) {
13580
+ fail(error, mode);
13581
+ }
13582
+ });
13583
+ workspaces.command("create <application>").description("Create a workspace when the target application policy permits it").requiredOption("--name <name>", "Workspace display name").option("--default", "Make the new workspace the application default").option("--confirm", "Acknowledge workspace creation").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (application, opts) => {
13584
+ const mode = resolveOutputMode(opts.json);
13585
+ try {
13586
+ const topolo = await requiredClient2();
13587
+ const result = await topolo.workspaces.create(application, {
13588
+ name: opts.name.trim(),
13589
+ isDefault: opts.default === true,
13590
+ confirm: opts.confirm === true
13591
+ });
13592
+ emit(result, mode, () => formatWorkspaceMutation(result));
13593
+ } catch (error) {
13594
+ fail(error, mode);
13595
+ }
13596
+ });
13597
+ workspaces.command("rename <application> <workspace-id>").description("Rename a caller-authorized workspace without changing its stable id").requiredOption("--name <name>", "New workspace display name").option("--confirm", "Acknowledge workspace rename").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (application, workspaceId, opts) => {
13598
+ const mode = resolveOutputMode(opts.json);
13599
+ try {
13600
+ const topolo = await requiredClient2();
13601
+ const result = await topolo.workspaces.rename(application, workspaceId, opts.name.trim(), opts.confirm === true);
13602
+ emit(result, mode, () => formatWorkspaceMutation(result));
13603
+ } catch (error) {
13604
+ fail(error, mode);
13605
+ }
13606
+ });
13607
+ workspaces.command("default <application> <workspace-id>").description("Set the default workspace for one application").option("--confirm", "Acknowledge the default-workspace change").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (application, workspaceId, opts) => {
13608
+ const mode = resolveOutputMode(opts.json);
13609
+ try {
13610
+ const topolo = await requiredClient2();
13611
+ const result = await topolo.workspaces.setDefault(application, workspaceId, opts.confirm === true);
13612
+ emit(result, mode, () => formatWorkspaceMutation(result));
13613
+ } catch (error) {
13614
+ fail(error, mode);
13615
+ }
13616
+ });
13617
+ }
13618
+ async function requiredClient2() {
13619
+ const topolo = await buildClient();
13620
+ if (!topolo) throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
13621
+ return topolo;
13622
+ }
13623
+ function formatWorkspaceList(result) {
13624
+ const { data } = result;
13625
+ const lines = [
13626
+ `Application: ${data.appId}`,
13627
+ `Organization: ${data.organizationId}`,
13628
+ `Can create: ${data.policy.canCreate ? "yes" : "no"} (${data.policy.reason})`
13629
+ ];
13630
+ if (!data.workspaces.length) return [...lines, "", "No accessible workspaces."].join("\n");
13631
+ lines.push("", "DEFAULT ID NAME ACCESS");
13632
+ for (const workspace of data.workspaces) {
13633
+ lines.push(`${workspace.isDefault ? "yes" : "no"} ${workspace.id} ${workspace.name} ${workspace.accessLevel}`);
13634
+ }
13635
+ return lines.join("\n");
13636
+ }
13637
+ function formatWorkspaceMutation(result) {
13638
+ const workspace = result.data.workspace;
13639
+ return `${result.message}
13640
+ Workspace: ${workspace.name} (${workspace.id})${workspace.isDefault ? "\nDefault: yes" : ""}`;
13641
+ }
13642
+
13464
13643
  // src/index.ts
13465
13644
  var program = new Command();
13466
13645
  var argv;
@@ -13492,6 +13671,7 @@ registerSeedCommands(program);
13492
13671
  registerSkillsCommand(program);
13493
13672
  registerContextCommand(program);
13494
13673
  registerResourcesCommand(program);
13674
+ registerWorkspacesCommand(program);
13495
13675
  program.parseAsync(argv).catch((err) => {
13496
13676
  process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
13497
13677
  `);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.6.8",
4
- "topoloPublishedVersion": "0.6.8",
3
+ "version": "0.7.0",
4
+ "topoloPublishedVersion": "0.7.0",
5
5
  "description": "Command-line interface for Topolo developers, operators, and agents, with internal first-party scaffolding available under the topolo platform namespace.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -4,7 +4,7 @@ This file teaches an agent how to interact with the Topolo platform via the `top
4
4
 
5
5
  ## What the platform is
6
6
 
7
- Topolo is a multi-app SaaS platform. The CLI and MCP server both use `@topolo/sdk`; bundled app IDs come from the generated registry, and newly registered API/technical services can be resolved from the live Auth catalog.
7
+ Topolo is a multi-app SaaS platform. The CLI and MCP server both use `@topolo/sdk`; every application resolves from the live credential-scoped registry, so newly registered apps and API/technical services work immediately — there is no bundled application catalog to rely on or wait for.
8
8
 
9
9
  - Use `topolo apps --json` or the MCP `topolo_search_applications` tool to discover platform applications, including browser-only Worker apps.
10
10
  - Use `topolo apps requirements <application_id> --json` or the MCP `topolo_list_application_requirements` tool before creating or expanding a Topolo app.
@@ -12,7 +12,7 @@ Topolo is a multi-app SaaS platform. The CLI and MCP server both use `@topolo/sd
12
12
  - Use `topolo services --json` or the MCP `topolo_search_applications` tool to discover callable app ids and effective URLs.
13
13
  - Use `topolo actions --service <app> --json` or MCP `topolo_search_actions` to discover callable operations granted to the current credential; invoke automatic reads with `topolo_read_action` and confirmation-gated operations with `topolo_call_action`.
14
14
  - Use `topolo actions capabilities`, `actions examples`, `actions validate`, and `actions plan` (or their MCP equivalents) to obtain schemas, examples, verification, and rollback before execution.
15
- - The generic `api` command and MCP `topolo_api_call` tool can reach bundled SDK services and live Auth-catalog API/technical services when no action contract exists yet.
15
+ - The generic `api` command and MCP `topolo_api_call` tool can reach any live Auth-catalog application or API/technical service when no action contract exists yet; discover ids with `topolo apps --json` first.
16
16
 
17
17
  Every call is scoped to the organization embedded in the credential. There is no `--org` flag anywhere — attempting to target another org will fail.
18
18
 
@@ -72,12 +72,24 @@ topolo resources <application> --json
72
72
 
73
73
  Use the returned app id or slug. Read `apiKeyResources` to learn whether calls can be scoped to aliases such as a workspace, site, mailbox, brand, or venue. Use `topolo resources` to resolve the actual credential-scoped resource ids. With an API key it returns only the key's bound resources; with OAuth it uses the published Auth resource catalog. Never assume that an organization id is a valid app resource id.
74
74
 
75
+ Workspace lifecycle is canonically owned by Topolo Auth but authorized against the concrete target application. Never conclude that workspace provisioning is unavailable because the target app's own action catalog does not duplicate Auth actions. Always run:
76
+
77
+ ```bash
78
+ topolo workspaces list <application> --json
79
+ ```
80
+
81
+ Its `data.policy.canCreate` and `data.policy.reason` are the caller-specific creation decision for that application. Organization ownership does not imply permission to create workspaces for every app. When permitted, the first-class mutations are `topolo workspaces create`, `topolo workspaces rename`, and `topolo workspaces default`; each still requires confirmation and Auth performs the final runtime authorization.
82
+
75
83
  MCP equivalents:
76
84
 
77
85
  - `topolo_search_applications`
78
86
  - `topolo_get_application`
79
87
  - `topolo_get_resource_types`
80
88
  - `topolo_list_resources`
89
+ - `topolo_list_workspaces`
90
+ - `topolo_create_workspace`
91
+ - `topolo_rename_workspace`
92
+ - `topolo_set_default_workspace`
81
93
 
82
94
  ### 2. Discover credential-scoped actions
83
95