@stigmer/cli 3.16.0 → 3.17.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/agent-exec-flags.d.ts +2 -5
- package/commands/agent-exec-flags.d.ts.map +1 -1
- package/commands/agent-exec-flags.js +6 -13
- package/commands/agent-exec-flags.js.map +1 -1
- package/commands/connect.js +1 -1
- package/commands/connect.js.map +1 -1
- package/commands/install.d.ts +3 -0
- package/commands/install.d.ts.map +1 -0
- package/commands/install.js +68 -0
- package/commands/install.js.map +1 -0
- package/commands/marketplace.d.ts +3 -0
- package/commands/marketplace.d.ts.map +1 -0
- package/commands/marketplace.js +94 -0
- package/commands/marketplace.js.map +1 -0
- package/commands/push.js +2 -7
- package/commands/push.js.map +1 -1
- package/commands/run.js +0 -3
- package/commands/run.js.map +1 -1
- package/config/config.d.ts +19 -0
- package/config/config.d.ts.map +1 -1
- package/config/config.js +15 -0
- package/config/config.js.map +1 -1
- package/config/index.d.ts +1 -1
- package/config/index.d.ts.map +1 -1
- package/config/index.js.map +1 -1
- package/local/bootstrap.d.ts +21 -0
- package/local/bootstrap.d.ts.map +1 -0
- package/local/bootstrap.js +88 -0
- package/local/bootstrap.js.map +1 -0
- package/local/daemon/launch.d.ts +2 -2
- package/local/daemon/launch.d.ts.map +1 -1
- package/local/daemon/launch.js +4 -34
- package/local/daemon/launch.js.map +1 -1
- package/local/daemon/process.d.ts +1 -1
- package/local/plugins/defaults.d.ts +35 -0
- package/local/plugins/defaults.d.ts.map +1 -0
- package/local/plugins/defaults.js +83 -0
- package/local/plugins/defaults.js.map +1 -0
- package/local/seedpack/content.d.ts +2 -1
- package/local/seedpack/content.d.ts.map +1 -1
- package/local/seedpack/content.js +5 -34
- package/local/seedpack/content.js.map +1 -1
- package/marketplace/config.d.ts +59 -0
- package/marketplace/config.d.ts.map +1 -0
- package/marketplace/config.js +162 -0
- package/marketplace/config.js.map +1 -0
- package/marketplace/github.d.ts +31 -0
- package/marketplace/github.d.ts.map +1 -0
- package/marketplace/github.js +203 -0
- package/marketplace/github.js.map +1 -0
- package/marketplace/index.d.ts +8 -0
- package/marketplace/index.d.ts.map +1 -0
- package/marketplace/index.js +12 -0
- package/marketplace/index.js.map +1 -0
- package/marketplace/install.d.ts +28 -0
- package/marketplace/install.d.ts.map +1 -0
- package/marketplace/install.js +109 -0
- package/marketplace/install.js.map +1 -0
- package/marketplace/materialize.d.ts +19 -0
- package/marketplace/materialize.d.ts.map +1 -0
- package/marketplace/materialize.js +53 -0
- package/marketplace/materialize.js.map +1 -0
- package/marketplace/official.d.ts +28 -0
- package/marketplace/official.d.ts.map +1 -0
- package/marketplace/official.js +86 -0
- package/marketplace/official.js.map +1 -0
- package/marketplace/read.d.ts +17 -0
- package/marketplace/read.d.ts.map +1 -0
- package/marketplace/read.js +44 -0
- package/marketplace/read.js.map +1 -0
- package/marketplace/ref.d.ts +13 -0
- package/marketplace/ref.d.ts.map +1 -0
- package/marketplace/ref.js +54 -0
- package/marketplace/ref.js.map +1 -0
- package/marketplace/render.d.ts +22 -0
- package/marketplace/render.d.ts.map +1 -0
- package/marketplace/render.js +118 -0
- package/marketplace/render.js.map +1 -0
- package/output/ndjson.js +1 -1
- package/output/ndjson.js.map +1 -1
- package/package.json +7 -6
- package/program.d.ts.map +1 -1
- package/program.js +4 -2
- package/program.js.map +1 -1
- package/resources/ignore/index.d.ts +2 -4
- package/resources/ignore/index.d.ts.map +1 -1
- package/resources/ignore/index.js +5 -5
- package/resources/ignore/index.js.map +1 -1
- package/resources/ignore/matcher.d.ts +2 -35
- package/resources/ignore/matcher.d.ts.map +1 -1
- package/resources/ignore/matcher.js +25 -109
- package/resources/ignore/matcher.js.map +1 -1
- package/resources/plugin.d.ts +50 -18
- package/resources/plugin.d.ts.map +1 -1
- package/resources/plugin.js +73 -74
- package/resources/plugin.js.map +1 -1
- package/resources/run/agent-exec.js +1 -1
- package/resources/run/agent-exec.js.map +1 -1
- package/resources/run/prepare.d.ts +8 -19
- package/resources/run/prepare.d.ts.map +1 -1
- package/resources/run/prepare.js +4 -4
- package/resources/run/prepare.js.map +1 -1
- package/resources/skill.d.ts +2 -2
- package/resources/skill.d.ts.map +1 -1
- package/resources/skill.js +6 -10
- package/resources/skill.js.map +1 -1
- package/src/commands/agent-exec-flags.ts +6 -12
- package/src/commands/connect.ts +1 -1
- package/src/commands/install.test.ts +289 -0
- package/src/commands/install.ts +108 -0
- package/src/commands/marketplace.test.ts +200 -0
- package/src/commands/marketplace.ts +131 -0
- package/src/commands/push.ts +5 -10
- package/src/commands/run.ts +0 -3
- package/src/config/config.ts +37 -0
- package/src/config/index.ts +1 -0
- package/src/local/bootstrap.test.ts +71 -0
- package/src/local/bootstrap.ts +133 -0
- package/src/local/daemon/launch.ts +5 -40
- package/src/local/daemon/process.ts +1 -1
- package/src/local/plugins/defaults.test.ts +238 -0
- package/src/local/plugins/defaults.ts +135 -0
- package/src/local/seedpack/content.ts +11 -42
- package/src/marketplace/__fixtures__/cursor-marketplace.ts +62 -0
- package/src/marketplace/config.test.ts +245 -0
- package/src/marketplace/config.ts +236 -0
- package/src/marketplace/github.test.ts +318 -0
- package/src/marketplace/github.ts +296 -0
- package/src/marketplace/index.ts +54 -0
- package/src/marketplace/install.test.ts +194 -0
- package/src/marketplace/install.ts +159 -0
- package/src/marketplace/materialize.ts +80 -0
- package/src/marketplace/official.test.ts +107 -0
- package/src/marketplace/official.ts +125 -0
- package/src/marketplace/read.test.ts +94 -0
- package/src/marketplace/read.ts +76 -0
- package/src/marketplace/ref.test.ts +179 -0
- package/src/marketplace/ref.ts +74 -0
- package/src/marketplace/render.ts +154 -0
- package/src/output/ndjson.ts +1 -1
- package/src/program.test.ts +4 -13
- package/src/program.ts +4 -2
- package/src/resources/ignore/ignore.test.ts +23 -100
- package/src/resources/ignore/index.ts +11 -8
- package/src/resources/ignore/matcher.ts +33 -136
- package/src/resources/plugin.test.ts +35 -6
- package/src/resources/plugin.ts +132 -85
- package/src/resources/run/agent-exec.ts +1 -1
- package/src/resources/run/prepare.test.ts +4 -18
- package/src/resources/run/prepare.ts +12 -24
- package/src/resources/skill.ts +6 -10
- package/commands/draft.d.ts +0 -3
- package/commands/draft.d.ts.map +0 -1
- package/commands/draft.js +0 -115
- package/commands/draft.js.map +0 -1
- package/resources/ignore/defaults.d.ts +0 -2
- package/resources/ignore/defaults.d.ts.map +0 -1
- package/resources/ignore/defaults.js +0 -140
- package/resources/ignore/defaults.js.map +0 -1
- package/resources/ignore/match.d.ts +0 -3
- package/resources/ignore/match.d.ts.map +0 -1
- package/resources/ignore/match.js +0 -166
- package/resources/ignore/match.js.map +0 -1
- package/resources/ignore/pattern.d.ts +0 -13
- package/resources/ignore/pattern.d.ts.map +0 -1
- package/resources/ignore/pattern.js +0 -118
- package/resources/ignore/pattern.js.map +0 -1
- package/src/commands/draft.ts +0 -189
- package/src/resources/ignore/defaults.ts +0 -155
- package/src/resources/ignore/match.ts +0 -158
- package/src/resources/ignore/pattern.ts +0 -131
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// `stigmer marketplace add|list|show|remove` — the plugin marketplaces this
|
|
2
|
+
// CLI installs from. A noun group, like `config` and `auth`, because a
|
|
3
|
+
// marketplace is client-side configuration rather than a resource kind: the
|
|
4
|
+
// server never sees one, only the plugin archive a client pushes from it.
|
|
5
|
+
//
|
|
6
|
+
// `list` is offline (the config file alone). `add` and `show` fetch the tree
|
|
7
|
+
// once, so `add` also proves the source is a readable marketplace before it
|
|
8
|
+
// is recorded, and the recorded name is the one the marketplace file gives
|
|
9
|
+
// itself unless `--name` says otherwise. Nothing is cached; `show` reads the
|
|
10
|
+
// source every time and says so in its docs.
|
|
11
|
+
//
|
|
12
|
+
// Heavy modules load lazily inside the action so `--help` stays fast.
|
|
13
|
+
|
|
14
|
+
import type { Command } from "commander";
|
|
15
|
+
import { type OutputFlags, renderResult } from "../output/index.js";
|
|
16
|
+
import { addResultFlags, resultFormat } from "./shared.js";
|
|
17
|
+
|
|
18
|
+
interface AddFlags extends OutputFlags {
|
|
19
|
+
name?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function registerMarketplace(program: Command): void {
|
|
23
|
+
const marketplace = program
|
|
24
|
+
.command("marketplace")
|
|
25
|
+
.description("manage the plugin marketplaces you install from");
|
|
26
|
+
|
|
27
|
+
const add = marketplace
|
|
28
|
+
.command("add <source>")
|
|
29
|
+
.description(
|
|
30
|
+
"add a marketplace: a directory, a GitHub 'owner/repo[@ref]', or that repository's URL",
|
|
31
|
+
)
|
|
32
|
+
.option(
|
|
33
|
+
"--name <name>",
|
|
34
|
+
"the name to install from (defaults to the name the marketplace file gives itself)",
|
|
35
|
+
)
|
|
36
|
+
.action((source: string, options: AddFlags) => runAdd(source, options));
|
|
37
|
+
addResultFlags(add);
|
|
38
|
+
|
|
39
|
+
const list = marketplace
|
|
40
|
+
.command("list")
|
|
41
|
+
.description("list the configured marketplaces (offline)")
|
|
42
|
+
.action((options: OutputFlags) => runList(options));
|
|
43
|
+
addResultFlags(list);
|
|
44
|
+
|
|
45
|
+
const show = marketplace
|
|
46
|
+
.command("show <name>")
|
|
47
|
+
.description("show the plugins a marketplace offers (reads its source)")
|
|
48
|
+
.action((name: string, options: OutputFlags) => runShow(name, options));
|
|
49
|
+
addResultFlags(show);
|
|
50
|
+
|
|
51
|
+
const remove = marketplace
|
|
52
|
+
.command("remove <name>")
|
|
53
|
+
.description("remove a configured marketplace (installed plugins stay)")
|
|
54
|
+
.action((name: string, options: OutputFlags) => runRemove(name, options));
|
|
55
|
+
addResultFlags(remove);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runAdd(source: string, flags: AddFlags): Promise<void> {
|
|
59
|
+
const format = resultFormat(flags);
|
|
60
|
+
const m = await import("../marketplace/index.js");
|
|
61
|
+
const { CommandResult } = await import("../output/index.js");
|
|
62
|
+
const { UsageError } = await import("../errors/index.js");
|
|
63
|
+
const { isValidPluginName } = await import("@stigmer/plugin-package");
|
|
64
|
+
|
|
65
|
+
const parsed = m.parseAddSource(source);
|
|
66
|
+
if (flags.name !== undefined && !isValidPluginName(flags.name)) {
|
|
67
|
+
throw new UsageError(
|
|
68
|
+
`'${flags.name}' is not a marketplace name\n\nA name is lowercase letters, digits, '.' and '-', like 'cursor-plugins'.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Read before recording: an entry that cannot be read is a mistake caught
|
|
73
|
+
// now, not on the first install, and the file's own name is the default.
|
|
74
|
+
const { name, offered } = await m.withMarketplace(parsed, (open) => {
|
|
75
|
+
const tree = m.readMarketplaceTree(open.root, m.describeSource(parsed));
|
|
76
|
+
return {
|
|
77
|
+
name: flags.name ?? tree.marketplace.name,
|
|
78
|
+
offered: tree.marketplace.plugins.length,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
m.addMarketplace(name, parsed);
|
|
82
|
+
|
|
83
|
+
const result = CommandResult.success(
|
|
84
|
+
`Added marketplace '${name}' (${m.describeSource(parsed)}), offering ${offered} plugin${offered === 1 ? "" : "s"}`,
|
|
85
|
+
);
|
|
86
|
+
result.hint(`See what it offers with: stigmer marketplace show ${name}`);
|
|
87
|
+
result.hint(`Install from it with: stigmer install ${name}/<plugin>`);
|
|
88
|
+
renderResult(
|
|
89
|
+
result.withData({ name, source: parsed, plugins: offered }),
|
|
90
|
+
format,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function runList(flags: OutputFlags): Promise<void> {
|
|
95
|
+
const format = resultFormat(flags);
|
|
96
|
+
const m = await import("../marketplace/index.js");
|
|
97
|
+
renderResult(m.renderMarketplaceList(m.listMarketplaces()), format);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function runShow(name: string, flags: OutputFlags): Promise<void> {
|
|
101
|
+
const format = resultFormat(flags);
|
|
102
|
+
const m = await import("../marketplace/index.js");
|
|
103
|
+
const { UsageError } = await import("../errors/index.js");
|
|
104
|
+
|
|
105
|
+
const marketplace = m.findMarketplace(name);
|
|
106
|
+
if (marketplace === undefined) {
|
|
107
|
+
throw new UsageError(
|
|
108
|
+
`no marketplace named '${name}' is configured\n\nRun 'stigmer marketplace list' to see the configured names.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const result = await m.withMarketplace(marketplace.source, (open) => {
|
|
112
|
+
const tree = m.readMarketplaceTree(
|
|
113
|
+
open.root,
|
|
114
|
+
m.describeSource(marketplace.source),
|
|
115
|
+
);
|
|
116
|
+
return m.renderMarketplaceShow(marketplace, tree, m.inspectEntries(tree));
|
|
117
|
+
});
|
|
118
|
+
renderResult(result, format);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function runRemove(name: string, flags: OutputFlags): Promise<void> {
|
|
122
|
+
const format = resultFormat(flags);
|
|
123
|
+
const m = await import("../marketplace/index.js");
|
|
124
|
+
const { CommandResult } = await import("../output/index.js");
|
|
125
|
+
m.removeMarketplace(name);
|
|
126
|
+
const result = CommandResult.success(`Removed marketplace '${name}'`);
|
|
127
|
+
result.hint(
|
|
128
|
+
"Plugins installed from it stay installed; remove one with: stigmer delete plugin <name>",
|
|
129
|
+
);
|
|
130
|
+
renderResult(result, format);
|
|
131
|
+
}
|
package/src/commands/push.ts
CHANGED
|
@@ -237,20 +237,15 @@ async function runPushPlugin(
|
|
|
237
237
|
const visibility = parseVisibility(options.visibility, "--visibility");
|
|
238
238
|
|
|
239
239
|
if (options.dryRun === true) {
|
|
240
|
-
const
|
|
241
|
-
const read = plugin.readPluginDirectory(directory, ignoreOptions);
|
|
242
|
-
const outcome = readPluginPackage(read.files);
|
|
243
|
-
if (!outcome.ok) {
|
|
244
|
-
throw plugin.pluginRefusal(directory, outcome.errors, outcome.warnings);
|
|
245
|
-
}
|
|
240
|
+
const prepared = await plugin.preparePluginPush(directory, ignoreOptions);
|
|
246
241
|
const { CommandResult } = await import("../output/index.js");
|
|
247
242
|
const result = plugin.describePackageOn(
|
|
248
243
|
CommandResult.success(
|
|
249
|
-
`Dry run: plugin '${
|
|
244
|
+
`Dry run: plugin '${prepared.plugin.name}' would install`,
|
|
250
245
|
),
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
246
|
+
prepared.plugin,
|
|
247
|
+
prepared.warnings,
|
|
248
|
+
prepared.stats,
|
|
254
249
|
);
|
|
255
250
|
result.hint("Run without --dry-run to install it.");
|
|
256
251
|
renderCommandResult(result, format);
|
package/src/commands/run.ts
CHANGED
|
@@ -194,9 +194,6 @@ async function runResolvedAgent(
|
|
|
194
194
|
accountPreferencesAvailable: await client.isResourceAvailable(
|
|
195
195
|
ApiResourceKind.identity_account,
|
|
196
196
|
),
|
|
197
|
-
// run opts into the account default_harness fill; draft deliberately
|
|
198
|
-
// does not (see the option's doc comment for the D5 rationale).
|
|
199
|
-
applyAccountHarnessDefault: true,
|
|
200
197
|
},
|
|
201
198
|
);
|
|
202
199
|
await executeResolvedAgent({
|
package/src/config/config.ts
CHANGED
|
@@ -83,11 +83,31 @@ export interface ContextConfig {
|
|
|
83
83
|
organization?: string;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* One added plugin marketplace as the file spells it. The `marketplace`
|
|
88
|
+
* commands are the only writer; the shape is kept loose here and narrowed by
|
|
89
|
+
* `src/marketplace/config.ts`, so a hand-edited entry this CLI cannot read is
|
|
90
|
+
* reported by `stigmer marketplace list` instead of being dropped on the next
|
|
91
|
+
* save. The official marketplace is built in and never stored. `type` is the
|
|
92
|
+
* discriminator, the word this file already uses for backends.
|
|
93
|
+
*/
|
|
94
|
+
export interface MarketplaceEntryConfig {
|
|
95
|
+
type?: string;
|
|
96
|
+
/** GitHub `owner/repo` for `type: github`. */
|
|
97
|
+
repo?: string;
|
|
98
|
+
/** Branch, tag or commit for `type: github`; the default branch when absent. */
|
|
99
|
+
ref?: string;
|
|
100
|
+
/** Absolute directory for `type: local`. */
|
|
101
|
+
path?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
86
104
|
export interface Config {
|
|
87
105
|
backend: BackendConfig;
|
|
88
106
|
backends?: Record<string, NamedBackendConfig>;
|
|
89
107
|
current_backend?: string;
|
|
90
108
|
context?: ContextConfig;
|
|
109
|
+
/** Added plugin marketplaces, keyed by the name a user installs from. */
|
|
110
|
+
marketplaces?: Record<string, MarketplaceEntryConfig>;
|
|
91
111
|
}
|
|
92
112
|
|
|
93
113
|
const SAVE_HEADER = `# Stigmer CLI Configuration
|
|
@@ -207,9 +227,25 @@ function normalize(parsed: Partial<Config> | null): Config {
|
|
|
207
227
|
backends,
|
|
208
228
|
current_backend: current,
|
|
209
229
|
context: parsed.context,
|
|
230
|
+
marketplaces: normalizeMarketplaces(parsed.marketplaces),
|
|
210
231
|
};
|
|
211
232
|
}
|
|
212
233
|
|
|
234
|
+
/** Object-valued entries verbatim; anything else is not a marketplace and is dropped. */
|
|
235
|
+
function normalizeMarketplaces(
|
|
236
|
+
parsed: Record<string, MarketplaceEntryConfig> | undefined,
|
|
237
|
+
): Record<string, MarketplaceEntryConfig> | undefined {
|
|
238
|
+
if (parsed === undefined || parsed === null || typeof parsed !== "object") {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
const marketplaces: Record<string, MarketplaceEntryConfig> = {};
|
|
242
|
+
for (const [name, entry] of Object.entries(parsed)) {
|
|
243
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
244
|
+
marketplaces[name] = { ...entry };
|
|
245
|
+
}
|
|
246
|
+
return Object.keys(marketplaces).length === 0 ? undefined : marketplaces;
|
|
247
|
+
}
|
|
248
|
+
|
|
213
249
|
function normalizeBackends(
|
|
214
250
|
parsed: Record<string, NamedBackendConfig> | undefined,
|
|
215
251
|
): Record<string, NamedBackendConfig> {
|
|
@@ -243,6 +279,7 @@ function serializable(config: Config): Config {
|
|
|
243
279
|
},
|
|
244
280
|
...(pristineLocal ? {} : { backends, current_backend: current }),
|
|
245
281
|
context: config.context,
|
|
282
|
+
marketplaces: config.marketplaces,
|
|
246
283
|
};
|
|
247
284
|
}
|
|
248
285
|
|
package/src/config/index.ts
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Pins the bootstrap's shape: the seedpack step then the default-plugins step,
|
|
2
|
+
// over one client; a seedpack failure is contained by its own step and the
|
|
3
|
+
// plugins step still runs; a plugins failure is a warning that names the
|
|
4
|
+
// retry command, never a throw past the bootstrap.
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import type { BackendClient } from "../client/index.js";
|
|
8
|
+
import {
|
|
9
|
+
bootstrapLocalBackend,
|
|
10
|
+
installDefaultPluginsBestEffort,
|
|
11
|
+
} from "./bootstrap.js";
|
|
12
|
+
|
|
13
|
+
const client = { stigmer: {} } as unknown as BackendClient;
|
|
14
|
+
|
|
15
|
+
describe("bootstrapLocalBackend", () => {
|
|
16
|
+
it("runs the seedpack step then the plugins step, over the same client and home", async () => {
|
|
17
|
+
const order: string[] = [];
|
|
18
|
+
await bootstrapLocalBackend("/home/x", {
|
|
19
|
+
client,
|
|
20
|
+
say: () => {},
|
|
21
|
+
seedpack: async (c, home) => {
|
|
22
|
+
expect(c).toBe(client);
|
|
23
|
+
expect(home).toBe("/home/x");
|
|
24
|
+
order.push("seedpack");
|
|
25
|
+
},
|
|
26
|
+
plugins: async (c, home) => {
|
|
27
|
+
expect(c).toBe(client);
|
|
28
|
+
expect(home).toBe("/home/x");
|
|
29
|
+
order.push("plugins");
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
expect(order).toEqual(["seedpack", "plugins"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("still runs the plugins step when the seedpack step has warned", async () => {
|
|
36
|
+
const said: string[] = [];
|
|
37
|
+
let plugins = 0;
|
|
38
|
+
await bootstrapLocalBackend("/home/x", {
|
|
39
|
+
client,
|
|
40
|
+
say: (line) => said.push(line),
|
|
41
|
+
seedpack: async (_c, _h, say) => say("Warning: seedpack failed"),
|
|
42
|
+
plugins: async () => {
|
|
43
|
+
plugins += 1;
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
expect(plugins).toBe(1);
|
|
47
|
+
expect(said).toEqual(["Warning: seedpack failed"]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("installDefaultPluginsBestEffort", () => {
|
|
52
|
+
it("turns a step that cannot even start into one warning naming the retry commands", async () => {
|
|
53
|
+
const said: string[] = [];
|
|
54
|
+
// A client whose plugin lookups throw: the step's own failure path, not a default's.
|
|
55
|
+
const broken = {
|
|
56
|
+
stigmer: {
|
|
57
|
+
plugin: {
|
|
58
|
+
getByReference: async () => {
|
|
59
|
+
throw new Error("backend unreachable");
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
} as unknown as BackendClient;
|
|
64
|
+
await installDefaultPluginsBestEffort(broken, "/home/x", (line) =>
|
|
65
|
+
said.push(line),
|
|
66
|
+
);
|
|
67
|
+
expect(said.filter((line) => line.startsWith("Warning:"))).toEqual([
|
|
68
|
+
"Warning: failed to install default plugin 'assistant'. Run 'stigmer install assistant' to retry.",
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// What `stigmer up` does once the local server answers: seed it. Two steps,
|
|
2
|
+
// in this order, each best-effort with its own warning, because the stack is
|
|
3
|
+
// already serving by the time either runs and a seeding failure must never
|
|
4
|
+
// tear it down:
|
|
5
|
+
//
|
|
6
|
+
// 1. the seedpack (the system org, the `stigmer` MCP server, the remaining
|
|
7
|
+
// system agents, skills and workflows), content-hash idempotent;
|
|
8
|
+
// 2. the default plugins the official marketplace names, installed into the
|
|
9
|
+
// same org as public, idempotent by the server's own digest.
|
|
10
|
+
//
|
|
11
|
+
// The seedpack goes first because the plugins install INTO the org it
|
|
12
|
+
// creates and reference the server it holds. When the seedpack retires, this
|
|
13
|
+
// file keeps step 2 and loses step 1, and nothing else moves.
|
|
14
|
+
//
|
|
15
|
+
// Both `up` shapes (detached and foreground) call this one function, so what
|
|
16
|
+
// a detached `up` seeds and what a container's `up --foreground` seeds cannot
|
|
17
|
+
// drift. Both always seed the local server `up` just started, never the
|
|
18
|
+
// active backend context: a user whose CLI is pointed at cloud must still get
|
|
19
|
+
// their local stack seeded, and must never have system resources applied to
|
|
20
|
+
// their cloud org. The client is pinned to a fresh local config
|
|
21
|
+
// (localhost:SERVER_PORT) with no auth, the trusted-local identity.
|
|
22
|
+
|
|
23
|
+
import type { BackendClient } from "../client/index.js";
|
|
24
|
+
import { log } from "../logger.js";
|
|
25
|
+
import { dataDir } from "./paths.js";
|
|
26
|
+
|
|
27
|
+
/** One seeding step: talks to the user through `say`, never throws past its own warning. */
|
|
28
|
+
export type BootstrapStep = (
|
|
29
|
+
client: BackendClient,
|
|
30
|
+
home: string,
|
|
31
|
+
say: Say,
|
|
32
|
+
) => Promise<void>;
|
|
33
|
+
|
|
34
|
+
export type Say = (line: string) => void;
|
|
35
|
+
|
|
36
|
+
/** Seams of the bootstrap, injectable for tests; the defaults are the real steps over the real local client. */
|
|
37
|
+
export interface BootstrapDeps {
|
|
38
|
+
readonly client?: BackendClient;
|
|
39
|
+
readonly seedpack?: BootstrapStep;
|
|
40
|
+
readonly plugins?: BootstrapStep;
|
|
41
|
+
readonly say?: Say;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sayToStderr: Say = (line) => {
|
|
45
|
+
process.stderr.write(`${line}\n`);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export async function bootstrapLocalBackend(
|
|
49
|
+
home: string,
|
|
50
|
+
deps: BootstrapDeps = {},
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
const client = deps.client ?? (await freshLocalClient());
|
|
53
|
+
const say = deps.say ?? sayToStderr;
|
|
54
|
+
await (deps.seedpack ?? applySeedpackBestEffort)(client, home, say);
|
|
55
|
+
await (deps.plugins ?? installDefaultPluginsBestEffort)(client, home, say);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function freshLocalClient(): Promise<BackendClient> {
|
|
59
|
+
const [{ createBackendClient }, { getDefault }] = await Promise.all([
|
|
60
|
+
import("../client/index.js"),
|
|
61
|
+
import("../config/config.js"),
|
|
62
|
+
]);
|
|
63
|
+
return createBackendClient({
|
|
64
|
+
config: getDefault(),
|
|
65
|
+
getAccessToken: () => null,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Step 1. Idempotent via a content-hash marker, so repeated `up`s are cheap. */
|
|
70
|
+
export async function applySeedpackBestEffort(
|
|
71
|
+
client: BackendClient,
|
|
72
|
+
home: string,
|
|
73
|
+
say: Say,
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
try {
|
|
76
|
+
const { applySeedpack } = await import("./seedpack/apply.js");
|
|
77
|
+
const result = await applySeedpack(
|
|
78
|
+
{
|
|
79
|
+
controller: client.controller,
|
|
80
|
+
stigmer: client.stigmer,
|
|
81
|
+
info: say,
|
|
82
|
+
warn: say,
|
|
83
|
+
},
|
|
84
|
+
{ markerDir: dataDir(home), home },
|
|
85
|
+
);
|
|
86
|
+
log.debug("seedpack bootstrap complete", {
|
|
87
|
+
applied: result.applied,
|
|
88
|
+
hash: result.hash,
|
|
89
|
+
org: result.org,
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
log.warn("seedpack bootstrap failed", { error: String(err) });
|
|
93
|
+
say(
|
|
94
|
+
"Warning: failed to apply system resources (seedpack). Run 'stigmer seedpack apply' to retry.",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Step 2. Runs even when step 1 warned: the plugins can only fail on their
|
|
101
|
+
* own terms (an org that does not exist fails the push, and the warning
|
|
102
|
+
* names the command that retries it).
|
|
103
|
+
*/
|
|
104
|
+
export async function installDefaultPluginsBestEffort(
|
|
105
|
+
client: BackendClient,
|
|
106
|
+
_home: string,
|
|
107
|
+
say: Say,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
try {
|
|
110
|
+
const [{ installDefaultPlugins }, { resolveSeedpackOrg }] =
|
|
111
|
+
await Promise.all([
|
|
112
|
+
import("./plugins/defaults.js"),
|
|
113
|
+
import("./seedpack/apply.js"),
|
|
114
|
+
]);
|
|
115
|
+
const result = await installDefaultPlugins(
|
|
116
|
+
{ stigmer: client.stigmer, info: say },
|
|
117
|
+
{ org: resolveSeedpackOrg() },
|
|
118
|
+
);
|
|
119
|
+
log.debug("default plugins bootstrap complete", {
|
|
120
|
+
outcomes: result.outcomes,
|
|
121
|
+
});
|
|
122
|
+
for (const name of result.failed) {
|
|
123
|
+
say(
|
|
124
|
+
`Warning: failed to install default plugin '${name}'. Run 'stigmer install ${name}' to retry.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
log.warn("default plugins bootstrap failed", { error: String(err) });
|
|
129
|
+
say(
|
|
130
|
+
"Warning: failed to install the default plugins. Run 'stigmer marketplace show stigmer' to see them and 'stigmer install <name>' to retry.",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -18,6 +18,7 @@ import { type Config, load as loadConfig } from "../../config/config.js";
|
|
|
18
18
|
import { log } from "../../logger.js";
|
|
19
19
|
import { CliExitError } from "../../errors/cli-exit-error.js";
|
|
20
20
|
import { ExitCode } from "../../errors/exit-codes.js";
|
|
21
|
+
import { bootstrapLocalBackend } from "../bootstrap.js";
|
|
21
22
|
import { DAEMON_PID_FILE, SERVER_PORT } from "../constants.js";
|
|
22
23
|
import { tcpConnects, waitForTcp } from "../net/tcp.js";
|
|
23
24
|
import { dataDir, logDir } from "../paths.js";
|
|
@@ -71,7 +72,7 @@ export async function up(options: UpOptions = {}, home: string = homedir()): Pro
|
|
|
71
72
|
|
|
72
73
|
await waitForTcp({ port: SERVER_PORT, timeoutMs: READY_TIMEOUT_MS, label: "stigmer-server" });
|
|
73
74
|
|
|
74
|
-
await
|
|
75
|
+
await bootstrapLocalBackend(home);
|
|
75
76
|
|
|
76
77
|
saveStartupConfig(data, buildStartupConfig(data, logs, temporalAddress, daemonPid, options));
|
|
77
78
|
}
|
|
@@ -82,7 +83,7 @@ export interface UpForegroundDeps {
|
|
|
82
83
|
runDaemon?: (deps: InternalDaemonDeps) => Promise<number>;
|
|
83
84
|
/** Resolves when the stack should shut down (default: the first SIGTERM/SIGINT). */
|
|
84
85
|
waitForShutdown?: () => Promise<void>;
|
|
85
|
-
/** Post-readiness bootstrap (default: the
|
|
86
|
+
/** Post-readiness bootstrap (default: `bootstrapLocalBackend`, the seedpack then the default plugins). */
|
|
86
87
|
bootstrap?: (home: string) => Promise<void>;
|
|
87
88
|
/** Receives each line the server and runner write (default: a `[component]`-prefixed stdout mirror). */
|
|
88
89
|
mirror?: OutputMirror;
|
|
@@ -99,7 +100,7 @@ export interface UpForegroundDeps {
|
|
|
99
100
|
* `up`, so `stigmer status` and `stigmer down` from another shell see an
|
|
100
101
|
* ordinary daemon whose PID happens to be ours. The children receive the very
|
|
101
102
|
* environment a detached daemon would have inherited, and what a detached `up`
|
|
102
|
-
* does once the server answers — the
|
|
103
|
+
* does once the server answers — the bootstrap, the startup record — happens in
|
|
103
104
|
* `onStarted`, at the same point in the stack's life.
|
|
104
105
|
*/
|
|
105
106
|
export async function upForeground(
|
|
@@ -109,7 +110,7 @@ export async function upForeground(
|
|
|
109
110
|
): Promise<number> {
|
|
110
111
|
const { data, logs, temporalAddress, env } = await prepareLaunch(options, home);
|
|
111
112
|
const runDaemon = deps.runDaemon ?? runInternalDaemon;
|
|
112
|
-
const bootstrap = deps.bootstrap ??
|
|
113
|
+
const bootstrap = deps.bootstrap ?? bootstrapLocalBackend;
|
|
113
114
|
|
|
114
115
|
return runDaemon({
|
|
115
116
|
config: readDaemonConfig(env),
|
|
@@ -179,42 +180,6 @@ async function prepareLaunch(options: UpOptions, home: string): Promise<Prepared
|
|
|
179
180
|
return { data, logs, temporalAddress: temporal.address, env };
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
/**
|
|
183
|
-
* Bootstrap the system seedpack into the freshly-started local backend.
|
|
184
|
-
* Idempotent via a content-hash marker, so repeated `up`s are cheap. Best-effort: the stack is
|
|
185
|
-
* already serving by this point, so a seedpack failure is surfaced as a warning
|
|
186
|
-
* with a retry hint rather than tearing the stack down.
|
|
187
|
-
*/
|
|
188
|
-
async function applySeedpackBestEffort(home: string): Promise<void> {
|
|
189
|
-
try {
|
|
190
|
-
const [{ createBackendClient }, { getDefault }, { applySeedpack }] = await Promise.all([
|
|
191
|
-
import("../../client/index.js"),
|
|
192
|
-
import("../../config/config.js"),
|
|
193
|
-
import("../seedpack/apply.js"),
|
|
194
|
-
]);
|
|
195
|
-
// Always seed the local server `up` just started — never the active backend
|
|
196
|
-
// context. A user whose CLI is pointed at cloud must still get their local
|
|
197
|
-
// stack seeded, and must never have system resources applied to their cloud
|
|
198
|
-
// org. Pin to a fresh local config (localhost:SERVER_PORT) with no auth.
|
|
199
|
-
const client = createBackendClient({ config: getDefault(), getAccessToken: () => null });
|
|
200
|
-
const result = await applySeedpack(
|
|
201
|
-
{
|
|
202
|
-
controller: client.controller,
|
|
203
|
-
stigmer: client.stigmer,
|
|
204
|
-
info: (line) => process.stderr.write(`${line}\n`),
|
|
205
|
-
warn: (line) => process.stderr.write(`${line}\n`),
|
|
206
|
-
},
|
|
207
|
-
{ markerDir: dataDir(home), home },
|
|
208
|
-
);
|
|
209
|
-
log.debug("seedpack bootstrap complete", { applied: result.applied, hash: result.hash, org: result.org });
|
|
210
|
-
} catch (err) {
|
|
211
|
-
log.warn("seedpack bootstrap failed", { error: String(err) });
|
|
212
|
-
process.stderr.write(
|
|
213
|
-
"Warning: failed to apply system resources (seedpack). Run 'stigmer seedpack apply' to retry.\n",
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
183
|
/** Stop the local stack. Returns false if nothing was running. */
|
|
219
184
|
export async function down(home: string = homedir()): Promise<boolean> {
|
|
220
185
|
const data = dataDir(home);
|
|
@@ -52,7 +52,7 @@ export interface InternalDaemonDeps {
|
|
|
52
52
|
/**
|
|
53
53
|
* Invoked once the stack is up and the monitor is running, and awaited
|
|
54
54
|
* before the daemon settles into waiting for shutdown. The foreground
|
|
55
|
-
* launcher does its post-readiness work here (
|
|
55
|
+
* launcher does its post-readiness work here (the bootstrap, the success card);
|
|
56
56
|
* tests use it to observe "started".
|
|
57
57
|
*/
|
|
58
58
|
onStarted?: () => void | Promise<void>;
|