@uniqbit/mate-core 0.15.1-canary.3 → 0.15.1-canary.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-core",
3
- "version": "0.15.1-canary.3",
3
+ "version": "0.15.1-canary.4",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
package/src/cli/main.ts CHANGED
@@ -22,16 +22,13 @@ import { hydrateDynamicPlugins } from "../tools/setup/dynamic-plugins/hydrate";
22
22
  import { findPluginCliCommand } from "./plugin-commands";
23
23
  import { usage } from "./usage";
24
24
 
25
- export function isInstallRecoveryCommand(command?: string, subcommand?: string): boolean {
26
- return (
27
- command === "install" ||
28
- command === "update" ||
29
- command === "doctor" ||
30
- command === "help" ||
31
- command === "--help" ||
32
- command === "-h" ||
33
- (command === "companion" && (subcommand === "setup" || subcommand === "link"))
34
- );
25
+ export interface GateNeeds {
26
+ /** Block the command while an enforced update is pending. */
27
+ updateGuard?: boolean;
28
+ /** Require an unambiguous companion (selection wizard on ambiguity) before dispatch. */
29
+ companion?: boolean;
30
+ /** Require a complete installation (install preflight) before dispatch. */
31
+ install?: boolean;
35
32
  }
36
33
 
37
34
  export interface MainDeps {
@@ -54,6 +51,11 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
54
51
  return;
55
52
  }
56
53
 
54
+ if (!command || command === "help" || command === "--help" || command === "-h") {
55
+ console.log(usage());
56
+ return;
57
+ }
58
+
57
59
  // Companion-declared plugins register before cap-command detection so
58
60
  // their commands route like compiled-in ones (including MCP servers whose
59
61
  // command is their own cap subcommand). Diagnostics stay on stderr; a
@@ -65,73 +67,102 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
65
67
  // are suppressed for them.
66
68
  const isPluginCommand =
67
69
  command === "cap" && findPluginCliCommand(subcommand, rest[0]) !== undefined;
68
- if (!isPluginCommand) {
69
- const updateStore = new UpdateStateStore();
70
- scheduleBackgroundCheck(updateStore);
71
- if (
72
- !isInstallRecoveryCommand(command, subcommand) &&
73
- (await enforceUpdateIfRequired(updateStore))
74
- ) {
75
- process.exitCode = 1;
76
- return;
77
- }
78
- await showUpdateBannerIfAvailable(updateStore);
79
- }
80
-
81
- if (!command || command === "help" || command === "--help" || command === "-h") {
82
- console.log(usage());
83
- return;
84
- }
85
70
 
86
- if (!(await deps.ensureUnambiguousCompanion())) {
87
- process.exitCode = 1;
88
- return;
89
- }
71
+ // Every dispatch case opens with a gate() call declaring what the command
72
+ // needs before it may run. Declared gates run in fixed order — update
73
+ // enforcement, companion selection, install preflight — and a blocked gate
74
+ // sets the exit code, so callers only need `if (!(await gate(...))) return`.
75
+ const gate = async (needs: GateNeeds): Promise<boolean> => {
76
+ if (!isPluginCommand) {
77
+ const updateStore = new UpdateStateStore();
78
+ scheduleBackgroundCheck(updateStore);
79
+ if (needs.updateGuard && (await enforceUpdateIfRequired(updateStore))) {
80
+ process.exitCode = 1;
81
+ return false;
82
+ }
83
+ await showUpdateBannerIfAvailable(updateStore);
84
+ }
90
85
 
91
- if (!isInstallRecoveryCommand(command, subcommand)) {
92
- const preflight = await deps.inspectInstallPreflight();
93
- if (!preflight.ok) {
94
- console.error(
95
- `${frameworkCommandName()}: ${preflight.reason ?? "installation is incomplete"}`,
96
- );
97
- console.error(`Run \`${frameworkCommandName()} install\` before continuing.`);
86
+ if (needs.companion && !(await deps.ensureUnambiguousCompanion())) {
98
87
  process.exitCode = 1;
99
- return;
88
+ return false;
100
89
  }
101
- }
90
+
91
+ if (needs.install) {
92
+ const preflight = await deps.inspectInstallPreflight();
93
+ if (!preflight.ok) {
94
+ console.error(
95
+ `${frameworkCommandName()}: ${preflight.reason ?? "installation is incomplete"}`,
96
+ );
97
+ console.error(`Run \`${frameworkCommandName()} install\` before continuing.`);
98
+ process.exitCode = 1;
99
+ return false;
100
+ }
101
+ }
102
+
103
+ return true;
104
+ };
102
105
 
103
106
  switch (command) {
104
107
  case "install":
108
+ // Builds a companion-scoped context, so it needs an unambiguous
109
+ // companion — but must stay runnable when installation is incomplete.
110
+ if (!(await gate({ companion: true }))) return;
105
111
  await runInstallCommand(argv.slice(3));
106
112
  return;
107
113
  case "artifact":
114
+ if (!(await gate({ updateGuard: true, companion: true, install: true }))) return;
108
115
  await runArtifactCommand(subcommand, rest);
109
116
  return;
110
117
  case "companion":
118
+ switch (subcommand) {
119
+ // setup/link are recovery paths and must never be gated on the
120
+ // state they exist to repair.
121
+ case "setup":
122
+ case "link":
123
+ if (!(await gate({}))) return;
124
+ break;
125
+ // open/tui consume a companion context.
126
+ case "open":
127
+ case "tui":
128
+ if (!(await gate({ updateGuard: true, companion: true }))) return;
129
+ break;
130
+ // list and unknown subcommands (which fail inside the command).
131
+ default:
132
+ if (!(await gate({ updateGuard: true }))) return;
133
+ }
111
134
  await runCompanionCommand(subcommand, rest);
112
135
  return;
113
136
  case "claude":
137
+ if (!(await gate({ updateGuard: true, companion: true, install: true }))) return;
114
138
  await runLaunchClaudeCommand(argv.slice(3), { directPassthrough: true });
115
139
  return;
116
140
  case "opencode":
141
+ if (!(await gate({ updateGuard: true, companion: true, install: true }))) return;
117
142
  await runLaunchOpenCodeCommand(argv.slice(3), { directPassthrough: true });
118
143
  return;
119
144
  case "report":
145
+ if (!(await gate({ updateGuard: true }))) return;
120
146
  await runReportCommand(argv.slice(3));
121
147
  return;
122
148
  case "config":
149
+ if (!(await gate({ updateGuard: true }))) return;
123
150
  await runConfigCommand(argv.slice(3));
124
151
  return;
125
152
  case "doctor":
153
+ if (!(await gate({}))) return;
126
154
  await runDoctorCommand(argv.slice(3));
127
155
  return;
128
156
  case "cap":
157
+ if (!(await gate({ updateGuard: true, companion: true, install: true }))) return;
129
158
  await runCapCommand(subcommand, rest);
130
159
  return;
131
160
  case "update":
161
+ if (!(await gate({}))) return;
132
162
  await runUpdateCommand(argv.slice(3));
133
163
  return;
134
164
  default:
165
+ // Unknown commands fail fast: no case matched, so no gate ever ran.
135
166
  console.error(`Unknown command: ${command}`);
136
167
  console.error(usage());
137
168
  process.exitCode = 1;
@@ -272,4 +272,5 @@ apply:
272
272
  tracks: tasks.md
273
273
  instruction: |
274
274
  Read context files and work through pending tasks only within the proposal's affected Areas. Mark complete as you go.
275
+ When reporting information to me, be extremely concise and sacrifice grammar for the sake of concision. Apply this same preference to JSDoc.
275
276
  Pause if you hit blockers or need clarification.
@@ -8,6 +8,7 @@ This block is kept for AGENTS.md compatibility and must not restate that policy.
8
8
  ## Agent Notes
9
9
 
10
10
  - Use absolute paths when a tool needs a file path. Do not create literal `$MATE_REPO_PATH` or `$MATE_ARTIFACT_PATH` directories.
11
+ - When reporting information to me, be extremely concise and sacrifice grammar for the sake of concision. Apply this same preference to JSDoc
11
12
  - Never add a `Co-Authored-By: <model>` trailer or model-attribution footer to commit messages.
12
13
  - Never commit, push, or open a pull request in the working repo (`$MATE_REPO_PATH`) unless the user explicitly asks for it.
13
14
 
@@ -8,6 +8,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
8
8
  ## Claude Notes
9
9
 
10
10
  - Use absolute paths when a tool needs a file path. Do not create literal `$MATE_REPO_PATH` or `$MATE_ARTIFACT_PATH` directories.
11
+ - When reporting information to me, be extremely concise and sacrifice grammar for the sake of concision. Apply this same preference to JSDoc.
11
12
  - Claude Code receives `$MATE_ARTIFACT_PATH` via `--add-dir`; if `@` autocomplete does not show companion artifacts, reference them by absolute path.
12
13
  - The root `CLAUDE.md` in the companion repo is intentional and loaded by Mate. Do not create another project-level `CLAUDE.md` in the working repo unless the user explicitly asks for one.
13
14
  - Never add a `Co-Authored-By: <model>` trailer or model-attribution footer to commit messages.
@@ -37,6 +37,3 @@ export function pluginLocalOverridesPath(companionPath: string): string {
37
37
 
38
38
  /** Companion-relative gitignore entry for the local override file. */
39
39
  export const PLUGIN_LOCAL_OVERRIDES_GITIGNORE_ENTRY = `.${FRAMEWORK_NAME}/config/plugins.local.yaml`;
40
-
41
- /** Companion-relative gitignore entry for installed plugin trees (regenerated by install). */
42
- export const PLUGIN_DEPENDENCIES_GITIGNORE_ENTRY = `.${FRAMEWORK_NAME}/dependencies/plugins/`;
@@ -1,8 +1,6 @@
1
1
  import { getActiveDistribution } from "../../../distribution";
2
- import {
3
- PLUGIN_DEPENDENCIES_GITIGNORE_ENTRY,
4
- PLUGIN_LOCAL_OVERRIDES_GITIGNORE_ENTRY,
5
- } from "../dynamic-plugins/paths";
2
+ import { FRAMEWORK_NAME } from "../../../framework";
3
+ import { PLUGIN_LOCAL_OVERRIDES_GITIGNORE_ENTRY } from "../dynamic-plugins/paths";
6
4
  import type { Plugin, SetupContext } from "../plugin";
7
5
 
8
6
  const MANAGED_START = (name: string) => `# ${name} managed: start`;
@@ -59,14 +57,22 @@ export async function writeManagedGitignoreBlock(
59
57
  }
60
58
 
61
59
  export function collectManagedGitignoreEntries(ctx: SetupContext, plugins: Plugin[]): string[] {
62
- const entries = plugins
63
- .filter((p) => p.kind !== "root" && (p.isEnabled(ctx.config) || p.persistGitignoreEntries))
64
- .flatMap((p) => p.gitignoreEntries?.(ctx) ?? []);
65
- // Dynamic-plugin loading artifacts: the local override file and installed
66
- // trees never version; the pin file (plugins.lock.yaml) stays committed and
67
- // is deliberately not listed here.
60
+ // Baseline for every companion: node_modules never versions wherever a tool
61
+ // materializes it, and everything under the dependencies tree (dynamic
62
+ // plugins, context-mode, future consumers) is regenerated by setup/install —
63
+ // version pins live in .mate/config and in code, never in the tree itself.
64
+ const entries = [
65
+ "node_modules/",
66
+ `.${FRAMEWORK_NAME}/dependencies/`,
67
+ ...plugins
68
+ .filter((p) => p.kind !== "root" && (p.isEnabled(ctx.config) || p.persistGitignoreEntries))
69
+ .flatMap((p) => p.gitignoreEntries?.(ctx) ?? []),
70
+ ];
71
+ // Dynamic-plugin loading artifacts: the local override file never versions;
72
+ // the pin file (plugins.lock.yaml) stays committed and is deliberately not
73
+ // listed here.
68
74
  if (ctx.config.plugins?.length) {
69
- entries.push(PLUGIN_LOCAL_OVERRIDES_GITIGNORE_ENTRY, PLUGIN_DEPENDENCIES_GITIGNORE_ENTRY);
75
+ entries.push(PLUGIN_LOCAL_OVERRIDES_GITIGNORE_ENTRY);
70
76
  }
71
77
  return entries;
72
78
  }