@stigmer/cli 3.23.1-dev.20260923075205 → 3.24.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/commands/bootstrap.d.ts.map +1 -1
- package/commands/bootstrap.js +16 -57
- package/commands/bootstrap.js.map +1 -1
- package/commands/config/context.d.ts.map +1 -1
- package/commands/config/context.js +41 -5
- package/commands/config/context.js.map +1 -1
- package/commands/up.js +1 -1
- package/commands/up.js.map +1 -1
- package/config/index.d.ts +1 -1
- package/config/index.d.ts.map +1 -1
- package/config/index.js +1 -1
- package/config/index.js.map +1 -1
- package/config/resolve.d.ts +8 -0
- package/config/resolve.d.ts.map +1 -1
- package/config/resolve.js +8 -8
- package/config/resolve.js.map +1 -1
- package/local/bootstrap.d.ts +9 -29
- package/local/bootstrap.d.ts.map +1 -1
- package/local/bootstrap.js +61 -69
- package/local/bootstrap.js.map +1 -1
- package/local/daemon/launch.d.ts +2 -2
- package/local/daemon/launch.d.ts.map +1 -1
- package/marketplace/official.d.ts.map +1 -1
- package/marketplace/official.js +6 -5
- package/marketplace/official.js.map +1 -1
- package/marketplace/render.d.ts.map +1 -1
- package/marketplace/render.js +0 -3
- package/marketplace/render.js.map +1 -1
- package/package.json +7 -7
- package/src/commands/bootstrap.test.ts +19 -75
- package/src/commands/bootstrap.ts +19 -68
- package/src/commands/config/context.test.ts +171 -0
- package/src/commands/config/context.ts +50 -5
- package/src/commands/marketplace.test.ts +2 -2
- package/src/commands/up.ts +1 -1
- package/src/config/index.ts +1 -0
- package/src/config/resolve.test.ts +2 -2
- package/src/config/resolve.ts +8 -8
- package/src/local/bootstrap.test.ts +132 -65
- package/src/local/bootstrap.ts +64 -139
- package/src/local/daemon/launch.ts +2 -2
- package/src/marketplace/official.ts +6 -5
- package/src/marketplace/read.test.ts +2 -3
- package/src/marketplace/render.ts +0 -3
- package/local/plugins/defaults.d.ts +0 -55
- package/local/plugins/defaults.d.ts.map +0 -1
- package/local/plugins/defaults.js +0 -111
- package/local/plugins/defaults.js.map +0 -1
- package/local/system-org.d.ts +0 -15
- package/local/system-org.d.ts.map +0 -1
- package/local/system-org.js +0 -57
- package/local/system-org.js.map +0 -1
- package/src/local/plugins/defaults.test.ts +0 -271
- package/src/local/plugins/defaults.ts +0 -179
- package/src/local/system-org.test.ts +0 -124
- package/src/local/system-org.ts +0 -63
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
// The default plugins a backend is bootstrapped with: the `defaults` list of
|
|
2
|
-
// the official marketplace, installed in order into the system org as
|
|
3
|
-
// public. The official list is empty today (a session with no agent runs
|
|
4
|
-
// the built-in assistant, so a fresh install needs no default content);
|
|
5
|
-
// the mechanism stays until the bootstrap is re-scoped.
|
|
6
|
-
//
|
|
7
|
-
// Two phases, deliberately separate. Preparing (resolve the official tree,
|
|
8
|
-
// which may acquire `@stigmer/plugins` at the CLI's version; walk, zip and
|
|
9
|
-
// hash every default) needs no backend and is all-or-nothing: the catalogue
|
|
10
|
-
// is ours and validated on CI, so a default that cannot be prepared is a
|
|
11
|
-
// release defect, not a condition to install around. Installing pushes the
|
|
12
|
-
// prepared archives and is best-effort per entry: one default failing to
|
|
13
|
-
// land does not stop the next, and the caller decides how to surface the
|
|
14
|
-
// failures. The split keeps every network read (the catalogue, the
|
|
15
|
-
// archives) ahead of the first write to the backend (local/bootstrap.ts).
|
|
16
|
-
//
|
|
17
|
-
// Idempotent by the server's own identity, not a marker file: before each
|
|
18
|
-
// push the org's plugin of that name is read back; when its digest equals the
|
|
19
|
-
// prepared archive's, it is READY and it is public, there is nothing to do
|
|
20
|
-
// and nothing is sent. A second `stigmer up` therefore pushes nothing, and a
|
|
21
|
-
// `stigmer up` after an upgrade pushes exactly the entries whose content
|
|
22
|
-
// changed. The record is the server, so any machine that reaches the same
|
|
23
|
-
// backend sees the same answer.
|
|
24
|
-
|
|
25
|
-
import { type Stigmer, isNotFound } from "@stigmer/sdk";
|
|
26
|
-
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
27
|
-
import { PluginState } from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/status_pb";
|
|
28
|
-
import { CliExitError } from "../../errors/cli-exit-error.js";
|
|
29
|
-
import { ExitCode } from "../../errors/exit-codes.js";
|
|
30
|
-
import { log } from "../../logger.js";
|
|
31
|
-
import { VERSION } from "../../version.js";
|
|
32
|
-
import {
|
|
33
|
-
type ResolveOfficialOptions,
|
|
34
|
-
resolveOfficialMarketplace,
|
|
35
|
-
} from "../../marketplace/official.js";
|
|
36
|
-
import { readMarketplaceTree } from "../../marketplace/read.js";
|
|
37
|
-
import { prepareEntry } from "../../marketplace/install.js";
|
|
38
|
-
import {
|
|
39
|
-
type PreparedPluginPush,
|
|
40
|
-
count,
|
|
41
|
-
pushPrepared,
|
|
42
|
-
} from "../../resources/plugin.js";
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* The command that ran the install, as the plugin's stored version message
|
|
46
|
-
* names it. Two verbs share the one bootstrap function, and a platform
|
|
47
|
-
* operator reading the row a year later must see which one wrote it.
|
|
48
|
-
*/
|
|
49
|
-
export type InstallingVerb = "stigmer up" | "stigmer bootstrap";
|
|
50
|
-
|
|
51
|
-
export interface DefaultPluginsDeps {
|
|
52
|
-
/** The client bound to the backend being bootstrapped. */
|
|
53
|
-
readonly stigmer: Stigmer;
|
|
54
|
-
/** Human progress lines (stderr). */
|
|
55
|
-
readonly info: (line: string) => void;
|
|
56
|
-
/** Stamped into each installed plugin's version message. */
|
|
57
|
-
readonly verb: InstallingVerb;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** One default, ready to push: its marketplace name and the prepared archive. */
|
|
61
|
-
export interface PreparedDefault {
|
|
62
|
-
readonly name: string;
|
|
63
|
-
readonly push: PreparedPluginPush;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export type DefaultPluginOutcome =
|
|
67
|
-
| {
|
|
68
|
-
readonly name: string;
|
|
69
|
-
readonly action: "installed";
|
|
70
|
-
readonly digest: string;
|
|
71
|
-
}
|
|
72
|
-
| {
|
|
73
|
-
readonly name: string;
|
|
74
|
-
readonly action: "up-to-date";
|
|
75
|
-
readonly digest: string;
|
|
76
|
-
}
|
|
77
|
-
| {
|
|
78
|
-
readonly name: string;
|
|
79
|
-
readonly action: "failed";
|
|
80
|
-
readonly error: string;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
export interface DefaultPluginsResult {
|
|
84
|
-
readonly outcomes: readonly DefaultPluginOutcome[];
|
|
85
|
-
/** The names that did not land, for the caller's warning. */
|
|
86
|
-
readonly failed: readonly string[];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Phase 1: every default the official marketplace names, prepared in the
|
|
91
|
-
* marketplace's order. Throws when the tree cannot be resolved, offers less
|
|
92
|
-
* than it lists (the library drops an entry it cannot install with a
|
|
93
|
-
* warning, which for a user's marketplace is `show`'s business and for ours
|
|
94
|
-
* is a broken release), or any default cannot be prepared; nothing is
|
|
95
|
-
* returned half-ready.
|
|
96
|
-
*/
|
|
97
|
-
export async function prepareDefaultPlugins(
|
|
98
|
-
official?: ResolveOfficialOptions,
|
|
99
|
-
): Promise<readonly PreparedDefault[]> {
|
|
100
|
-
const resolved = resolveOfficialMarketplace(official);
|
|
101
|
-
const tree = readMarketplaceTree(resolved.dir, "the official marketplace");
|
|
102
|
-
if (tree.warnings.length > 0) {
|
|
103
|
-
throw new CliExitError(
|
|
104
|
-
`the official marketplace at ${resolved.dir} lists ${count(tree.warnings.length, "plugin")} this CLI cannot install`,
|
|
105
|
-
ExitCode.General,
|
|
106
|
-
tree.warnings.map((finding) => finding.message),
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
const prepared: PreparedDefault[] = [];
|
|
110
|
-
for (const name of tree.marketplace.defaults) {
|
|
111
|
-
prepared.push({ name, push: await prepareEntry(tree, name) });
|
|
112
|
-
}
|
|
113
|
-
return prepared;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Phase 2: install the prepared defaults into `org`, skipping what the
|
|
118
|
-
* backend already holds. Best-effort per entry.
|
|
119
|
-
*/
|
|
120
|
-
export async function installPreparedDefaults(
|
|
121
|
-
deps: DefaultPluginsDeps,
|
|
122
|
-
prepared: readonly PreparedDefault[],
|
|
123
|
-
org: string,
|
|
124
|
-
): Promise<DefaultPluginsResult> {
|
|
125
|
-
const outcomes: DefaultPluginOutcome[] = [];
|
|
126
|
-
for (const { name, push } of prepared) {
|
|
127
|
-
try {
|
|
128
|
-
if (await isAlreadyInstalled(deps.stigmer, org, push)) {
|
|
129
|
-
outcomes.push({ name, action: "up-to-date", digest: push.digest });
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
deps.info(`Installing default plugin '${name}'…`);
|
|
133
|
-
await pushPrepared(deps.stigmer, push, {
|
|
134
|
-
org,
|
|
135
|
-
visibility: ApiResourceVisibility.visibility_public,
|
|
136
|
-
message: `default plugin, installed by ${deps.verb} (${VERSION})`,
|
|
137
|
-
});
|
|
138
|
-
outcomes.push({ name, action: "installed", digest: push.digest });
|
|
139
|
-
} catch (error) {
|
|
140
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
141
|
-
log.warn("default plugin install failed", { name, error: message });
|
|
142
|
-
outcomes.push({ name, action: "failed", error: message });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return {
|
|
146
|
-
outcomes,
|
|
147
|
-
failed: outcomes
|
|
148
|
-
.filter((outcome) => outcome.action === "failed")
|
|
149
|
-
.map((outcome) => outcome.name),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* True when the org already holds this exact archive, materialised and
|
|
155
|
-
* public. A plugin that is absent (NOT_FOUND) installs; one that is present
|
|
156
|
-
* with another digest, still installing, failed, or not public is pushed
|
|
157
|
-
* again, which the server treats as an upgrade or a convergence.
|
|
158
|
-
*/
|
|
159
|
-
async function isAlreadyInstalled(
|
|
160
|
-
stigmer: Stigmer,
|
|
161
|
-
org: string,
|
|
162
|
-
prepared: PreparedPluginPush,
|
|
163
|
-
): Promise<boolean> {
|
|
164
|
-
let existing;
|
|
165
|
-
try {
|
|
166
|
-
existing = await stigmer.plugin.getByReference({
|
|
167
|
-
org,
|
|
168
|
-
slug: prepared.plugin.name,
|
|
169
|
-
});
|
|
170
|
-
} catch (error) {
|
|
171
|
-
if (isNotFound(error)) return false;
|
|
172
|
-
throw error;
|
|
173
|
-
}
|
|
174
|
-
return (
|
|
175
|
-
existing.status?.digest === prepared.digest &&
|
|
176
|
-
existing.status.state === PluginState.READY &&
|
|
177
|
-
existing.metadata?.visibility === ApiResourceVisibility.visibility_public
|
|
178
|
-
);
|
|
179
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
// Pins the system org's "ensure" over a real Connect backend: an org the
|
|
2
|
-
// identity already sees is `present` with no create; an absent one is
|
|
3
|
-
// created as the Organization as code (slug, name, label, self-managed) and
|
|
4
|
-
// reported `created`; a create that loses the race to another launcher
|
|
5
|
-
// (ALREADY_EXISTS) is `present`, not an error; any other refusal propagates
|
|
6
|
-
// with the server's sentence.
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
createServer as createHttp2Server,
|
|
10
|
-
type Http2Server,
|
|
11
|
-
type ServerHttp2Session,
|
|
12
|
-
} from "node:http2";
|
|
13
|
-
import type { AddressInfo } from "node:net";
|
|
14
|
-
import { create } from "@bufbuild/protobuf";
|
|
15
|
-
import { Code, ConnectError, type ConnectRouter } from "@connectrpc/connect";
|
|
16
|
-
import { connectNodeAdapter } from "@connectrpc/connect-node";
|
|
17
|
-
import {
|
|
18
|
-
type Organization,
|
|
19
|
-
OrganizationSchema,
|
|
20
|
-
} from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
21
|
-
import { OrganizationCommandController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/command_pb";
|
|
22
|
-
import { ManagementMode } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/enum_pb";
|
|
23
|
-
import { OrganizationsSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/io_pb";
|
|
24
|
-
import { OrganizationQueryController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/query_pb";
|
|
25
|
-
import { createNodeClient, normalizeEndpoint } from "@stigmer/sdk/node";
|
|
26
|
-
import type { Stigmer } from "@stigmer/sdk";
|
|
27
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
28
|
-
import { SYSTEM_ORG, SYSTEM_ORG_LABEL, ensureSystemOrg } from "./system-org.js";
|
|
29
|
-
|
|
30
|
-
let backend: Http2Server;
|
|
31
|
-
let stigmer: Stigmer;
|
|
32
|
-
const openSessions = new Set<ServerHttp2Session>();
|
|
33
|
-
|
|
34
|
-
/** The orgs the backend holds, by slug. */
|
|
35
|
-
let held: Map<string, Organization>;
|
|
36
|
-
let creates: Organization[];
|
|
37
|
-
/** What `create` answers with instead of creating, when set. */
|
|
38
|
-
let refuseCreate: ConnectError | undefined;
|
|
39
|
-
|
|
40
|
-
beforeAll(async () => {
|
|
41
|
-
const routes = (router: ConnectRouter) => {
|
|
42
|
-
router.service(OrganizationQueryController, {
|
|
43
|
-
findMyOrganizations: () =>
|
|
44
|
-
create(OrganizationsSchema, { entries: [...held.values()] }),
|
|
45
|
-
});
|
|
46
|
-
router.service(OrganizationCommandController, {
|
|
47
|
-
create: (org) => {
|
|
48
|
-
creates.push(org);
|
|
49
|
-
if (refuseCreate !== undefined) throw refuseCreate;
|
|
50
|
-
const slug = org.metadata?.slug ?? "";
|
|
51
|
-
const row = create(OrganizationSchema, org);
|
|
52
|
-
if (row.metadata !== undefined) row.metadata.id = `org_${slug}`;
|
|
53
|
-
held.set(slug, row);
|
|
54
|
-
return row;
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
};
|
|
58
|
-
backend = createHttp2Server(connectNodeAdapter({ routes }));
|
|
59
|
-
backend.on("session", (session) => {
|
|
60
|
-
openSessions.add(session);
|
|
61
|
-
session.on("close", () => openSessions.delete(session));
|
|
62
|
-
});
|
|
63
|
-
await new Promise<void>((resolve) => backend.listen(0, "127.0.0.1", resolve));
|
|
64
|
-
const port = (backend.address() as AddressInfo).port;
|
|
65
|
-
stigmer = createNodeClient({
|
|
66
|
-
baseUrl: normalizeEndpoint(`127.0.0.1:${port}`),
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
afterAll(async () => {
|
|
71
|
-
for (const session of openSessions) session.destroy();
|
|
72
|
-
await new Promise<void>((resolve) => backend.close(() => resolve()));
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
beforeEach(() => {
|
|
76
|
-
held = new Map();
|
|
77
|
-
creates = [];
|
|
78
|
-
refuseCreate = undefined;
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("ensureSystemOrg", () => {
|
|
82
|
-
it("is present, with no create, when the identity already sees it", async () => {
|
|
83
|
-
held.set(
|
|
84
|
-
SYSTEM_ORG,
|
|
85
|
-
create(OrganizationSchema, { metadata: { slug: SYSTEM_ORG } }),
|
|
86
|
-
);
|
|
87
|
-
expect(await ensureSystemOrg(stigmer)).toBe("present");
|
|
88
|
-
expect(creates).toHaveLength(0);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("creates the Organization as code when absent", async () => {
|
|
92
|
-
held.set(
|
|
93
|
-
"acme",
|
|
94
|
-
create(OrganizationSchema, { metadata: { slug: "acme" } }),
|
|
95
|
-
);
|
|
96
|
-
expect(await ensureSystemOrg(stigmer)).toBe("created");
|
|
97
|
-
expect(creates).toHaveLength(1);
|
|
98
|
-
const sent = creates[0]!;
|
|
99
|
-
expect(sent.metadata?.slug).toBe(SYSTEM_ORG);
|
|
100
|
-
expect(sent.metadata?.org).toBe(SYSTEM_ORG);
|
|
101
|
-
expect(sent.metadata?.name).toBe("Stigmer");
|
|
102
|
-
expect(sent.metadata?.labels).toEqual({ [SYSTEM_ORG_LABEL]: "true" });
|
|
103
|
-
expect(sent.spec?.managementMode).toBe(ManagementMode.self_managed);
|
|
104
|
-
expect(sent.spec?.description).not.toBe("");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("treats a create lost to another launcher as present", async () => {
|
|
108
|
-
refuseCreate = new ConnectError(
|
|
109
|
-
`organization '${SYSTEM_ORG}' already exists`,
|
|
110
|
-
Code.AlreadyExists,
|
|
111
|
-
);
|
|
112
|
-
expect(await ensureSystemOrg(stigmer)).toBe("present");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("propagates any other refusal with the server's sentence", async () => {
|
|
116
|
-
refuseCreate = new ConnectError(
|
|
117
|
-
"reserved labels are platform-managed",
|
|
118
|
-
Code.PermissionDenied,
|
|
119
|
-
);
|
|
120
|
-
await expect(ensureSystemOrg(stigmer)).rejects.toThrow(
|
|
121
|
-
/reserved labels are platform-managed/,
|
|
122
|
-
);
|
|
123
|
-
});
|
|
124
|
-
});
|
package/src/local/system-org.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// The system organization, as code: the one `stigmer` org every Stigmer
|
|
2
|
-
// backend carries, which the official default plugins are installed into
|
|
3
|
-
// and the local CLI falls back to when no org is configured.
|
|
4
|
-
//
|
|
5
|
-
// This is the only place the org is defined. The bootstrap creates it when
|
|
6
|
-
// a backend does not have it (a fresh local data dir, a raw self-hosted
|
|
7
|
-
// server); on the hosted platform it already exists, owned by IAM, and the
|
|
8
|
-
// create never fires. Its slug is the same in both editions, so nothing
|
|
9
|
-
// here is configurable: a bootstrap that could be pointed at another org
|
|
10
|
-
// would put the platform's public defaults where no cross-org lookup finds
|
|
11
|
-
// them.
|
|
12
|
-
//
|
|
13
|
-
// "Ensure" is find-then-create with the race closed by the server: two
|
|
14
|
-
// launchers racing the same check see one `already-exists`, and that IS the
|
|
15
|
-
// desired end state, so it is reported as `present`, never rethrown. Any
|
|
16
|
-
// other refusal (a self-hosted authorizer that denies the reserved label,
|
|
17
|
-
// a backend that refuses the create) propagates with the server's sentence;
|
|
18
|
-
// the caller decides how loud to be.
|
|
19
|
-
|
|
20
|
-
import { ManagementMode } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/enum_pb";
|
|
21
|
-
import { type Stigmer, StigmerError } from "@stigmer/sdk";
|
|
22
|
-
|
|
23
|
-
/** The system organization's slug, identical across editions. */
|
|
24
|
-
export const SYSTEM_ORG = "stigmer";
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Marks the system org so an operator can tell it from a user's own org in
|
|
28
|
-
* any listing. Informational: no server or client code path reads it.
|
|
29
|
-
*/
|
|
30
|
-
export const SYSTEM_ORG_LABEL = "stigmer.ai/system";
|
|
31
|
-
|
|
32
|
-
export type EnsureSystemOrgOutcome = "present" | "created";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Make sure the system organization exists on the backend `stigmer` is bound
|
|
36
|
-
* to. Idempotent; a lost create race counts as `present`.
|
|
37
|
-
*/
|
|
38
|
-
export async function ensureSystemOrg(
|
|
39
|
-
stigmer: Stigmer,
|
|
40
|
-
): Promise<EnsureSystemOrgOutcome> {
|
|
41
|
-
const mine = await stigmer.organization.findMyOrganizations();
|
|
42
|
-
if (mine.entries.some((org) => org.metadata?.slug === SYSTEM_ORG)) {
|
|
43
|
-
return "present";
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
// Organizations are self-owning: the org field is the slug itself, the
|
|
47
|
-
// shape the console's create form and the e2e harness both use.
|
|
48
|
-
await stigmer.organization.create({
|
|
49
|
-
name: "Stigmer",
|
|
50
|
-
slug: SYSTEM_ORG,
|
|
51
|
-
org: SYSTEM_ORG,
|
|
52
|
-
description: "System organization for Stigmer installations",
|
|
53
|
-
labels: { [SYSTEM_ORG_LABEL]: "true" },
|
|
54
|
-
managementMode: ManagementMode.self_managed,
|
|
55
|
-
});
|
|
56
|
-
return "created";
|
|
57
|
-
} catch (error) {
|
|
58
|
-
if (error instanceof StigmerError && error.code === "already-exists") {
|
|
59
|
-
return "present";
|
|
60
|
-
}
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
63
|
-
}
|