@notis_ai/cli 0.2.0-beta.16.1 → 0.2.0-beta.160.1

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.
Files changed (154) hide show
  1. package/README.md +433 -133
  2. package/config/notis_app_boundary_rules.json +50 -0
  3. package/config/notis_app_design_rules.json +135 -0
  4. package/dist/agent-hooks/notis-agent-hook.mjs +18672 -0
  5. package/dist/base-skills/notis-apps/SKILL.md +70 -0
  6. package/dist/base-skills/notis-apps/references/architecture.md +164 -0
  7. package/dist/base-skills/notis-apps/references/context.md +81 -0
  8. package/dist/base-skills/notis-apps/references/design.md +165 -0
  9. package/dist/base-skills/notis-apps/references/reading.md +89 -0
  10. package/dist/base-skills/notis-apps/references/release.md +99 -0
  11. package/dist/base-skills/notis-apps/references/sdk.md +62 -0
  12. package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
  13. package/dist/base-skills/notis-cli/SKILL.md +140 -0
  14. package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
  15. package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
  16. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  17. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  18. package/dist/base-skills/notis-query/SKILL.md +67 -0
  19. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  20. package/dist/base-skills/notis-query/references/documents.md +50 -0
  21. package/dist/base-skills/notis-query/references/query.md +543 -0
  22. package/dist/skill-sync/index.js +1626 -0
  23. package/dist/skill-sync/index.js.map +7 -0
  24. package/dist/skill-sync-worker.mjs +2990 -0
  25. package/package.json +16 -6
  26. package/skills/notis-apps/cli.md +313 -0
  27. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  28. package/skills/notis-onboarding/BRIEF.md +129 -0
  29. package/skills/notis-query/cli.md +39 -0
  30. package/src/agent-hook-entry.js +5 -0
  31. package/src/cli.js +294 -25
  32. package/src/command-specs/agents.js +392 -0
  33. package/src/command-specs/apps.js +1470 -202
  34. package/src/command-specs/auth.js +114 -137
  35. package/src/command-specs/diagnostics.js +716 -0
  36. package/src/command-specs/handover.js +374 -0
  37. package/src/command-specs/helpers.js +84 -82
  38. package/src/command-specs/index.js +25 -6
  39. package/src/command-specs/meta.js +150 -18
  40. package/src/command-specs/onboarding.js +290 -0
  41. package/src/command-specs/profile.js +358 -0
  42. package/src/command-specs/reports.js +86 -0
  43. package/src/command-specs/skills.js +75 -0
  44. package/src/command-specs/smoke.js +386 -0
  45. package/src/command-specs/tools.js +455 -139
  46. package/src/runtime/agent-browser.js +632 -0
  47. package/src/runtime/agent-memory-state.js +126 -0
  48. package/src/runtime/agent-setup.js +383 -0
  49. package/src/runtime/app-boundary-validator.js +404 -0
  50. package/src/runtime/app-changelog.js +79 -0
  51. package/src/runtime/app-platform.js +2633 -210
  52. package/src/runtime/app-registry-scaffolds.js +367 -0
  53. package/src/runtime/app-test-server.js +292 -0
  54. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  55. package/src/runtime/auth-recovery.js +110 -0
  56. package/src/runtime/base-skills.d.ts +20 -0
  57. package/src/runtime/base-skills.js +167 -0
  58. package/src/runtime/channel.js +133 -0
  59. package/src/runtime/delegated-context.js +68 -0
  60. package/src/runtime/errors.js +1 -0
  61. package/src/runtime/git.js +233 -0
  62. package/src/runtime/login-listener.js +15 -0
  63. package/src/runtime/oauth.js +2622 -0
  64. package/src/runtime/output.js +37 -5
  65. package/src/runtime/ports.js +31 -0
  66. package/src/runtime/profiles.js +906 -55
  67. package/src/runtime/skill-sync/cloud-client.ts +99 -0
  68. package/src/runtime/skill-sync/index.ts +697 -0
  69. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  70. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  71. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  72. package/src/runtime/skill-sync/types.ts +110 -0
  73. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  74. package/src/runtime/skill-sync-service.js +109 -0
  75. package/src/runtime/store-screenshot.js +143 -0
  76. package/src/runtime/sync-skills.d.ts +37 -0
  77. package/src/runtime/sync-skills.js +231 -0
  78. package/src/runtime/telemetry.js +92 -0
  79. package/src/runtime/transport.js +324 -45
  80. package/src/skill-sync-worker-entry.js +2 -0
  81. package/src/skill-sync-worker.js +50 -0
  82. package/template/.harness/index.html.tmpl +430 -0
  83. package/template/CHANGELOG.md +5 -0
  84. package/template/app/layout.tsx +5 -2
  85. package/template/app/page.tsx +49 -42
  86. package/template/components/page-heading.tsx +23 -0
  87. package/template/components/ui/badge.tsx +7 -4
  88. package/template/components/ui/card.tsx +24 -11
  89. package/template/components/ui/native-select.tsx +24 -0
  90. package/template/notis.config.ts +24 -6
  91. package/template/package-lock.json +4137 -0
  92. package/template/package.json +5 -5
  93. package/template/packages/{notis-sdk → sdk}/package.json +13 -3
  94. package/template/packages/sdk/src/agentContext.ts +36 -0
  95. package/template/packages/sdk/src/components/DocumentEditor.tsx +103 -0
  96. package/template/packages/sdk/src/components/Markdown.tsx +60 -0
  97. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  98. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  99. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  100. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  101. package/template/packages/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  102. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  103. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  104. package/template/packages/sdk/src/components/Skeleton.tsx +24 -0
  105. package/template/packages/sdk/src/config.ts +257 -0
  106. package/template/packages/sdk/src/documents.ts +256 -0
  107. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  108. package/template/packages/sdk/src/hooks/useAgentContext.ts +23 -0
  109. package/template/packages/sdk/src/hooks/useCloudComputer.ts +64 -0
  110. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +836 -0
  111. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  112. package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  113. package/template/packages/sdk/src/hooks/useDocument.ts +43 -0
  114. package/template/packages/sdk/src/hooks/useDocuments.ts +84 -0
  115. package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
  116. package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
  117. package/template/packages/sdk/src/hooks/useMultiSelect.ts +95 -0
  118. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +10 -4
  119. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +11 -8
  120. package/template/packages/sdk/src/hooks/useQuery.ts +71 -0
  121. package/template/packages/sdk/src/hooks/useTool.ts +65 -0
  122. package/template/packages/sdk/src/hooks/useToolQuery.ts +12 -0
  123. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +81 -0
  124. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  125. package/template/packages/sdk/src/index.ts +161 -0
  126. package/template/packages/sdk/src/interactions/actions.ts +59 -0
  127. package/template/packages/sdk/src/interactions/shortcuts.tsx +694 -0
  128. package/template/packages/sdk/src/interactions/visibility.ts +13 -0
  129. package/template/packages/sdk/src/interactions.ts +45 -0
  130. package/template/packages/sdk/src/provider.tsx +44 -0
  131. package/template/packages/sdk/src/queryCache.ts +170 -0
  132. package/template/packages/sdk/src/runtime.ts +451 -0
  133. package/template/packages/sdk/src/styles.css +213 -0
  134. package/template/packages/sdk/src/tailwind.ts +56 -0
  135. package/template/packages/{notis-sdk → sdk}/src/vite.ts +5 -1
  136. package/template/tailwind.config.ts +1 -0
  137. package/src/command-specs/db.js +0 -163
  138. package/src/runtime/app-preview-server.js +0 -312
  139. package/template/packages/notis-sdk/src/config.ts +0 -48
  140. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  141. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  142. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  143. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  144. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  145. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  146. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  147. package/template/packages/notis-sdk/src/index.ts +0 -47
  148. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  149. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  150. package/template/packages/notis-sdk/src/styles.css +0 -123
  151. /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
  152. /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
  153. /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
  154. /package/template/packages/{notis-sdk → sdk}/tsconfig.json +0 -0
package/src/cli.js CHANGED
@@ -1,10 +1,47 @@
1
- import { Command } from 'commander';
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Command, Option } from 'commander';
2
6
  import { COMMAND_SPECS, GROUP_SUMMARIES } from './command-specs/index.js';
3
7
  import { OutputManager } from './runtime/output.js';
4
8
  import { asCliError } from './runtime/errors.js';
5
- import { resolveRuntimeProfile, workspacePath } from './runtime/profiles.js';
9
+ import { reportCliCommand } from './runtime/telemetry.js';
10
+ import { reconcileBaseSkillsBestEffort } from './runtime/base-skills.js';
11
+ import { detectedAgentIds } from './runtime/agent-setup.js';
12
+ import { withSkillSyncLock } from './runtime/sync-skills.js';
13
+ import { maybeInstallSkillSyncService } from './runtime/skill-sync-service.js';
14
+ import {
15
+ CHANNEL_SWITCH_ENV,
16
+ resolveChannelSwitch,
17
+ } from './runtime/channel.js';
18
+ import {
19
+ DEFAULT_PROFILE,
20
+ getProfile,
21
+ loadConfig,
22
+ resolveOutputMode,
23
+ resolveRuntimeProfile,
24
+ resolveWorktreeRuntime,
25
+ workspacePath,
26
+ } from './runtime/profiles.js';
6
27
 
7
- const CLI_VERSION = '0.2.0';
28
+ // Read the version from package.json: the publish pipeline bumps the manifest
29
+ // (scripts/release-utils.js applyVersion), so a hardcoded string here goes
30
+ // stale on every release.
31
+ function readCliVersion() {
32
+ try {
33
+ const manifestPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
34
+ const version = JSON.parse(readFileSync(manifestPath, 'utf-8')).version;
35
+ if (typeof version === 'string' && version.trim()) {
36
+ return version.trim();
37
+ }
38
+ } catch {
39
+ // Fall through to the placeholder below.
40
+ }
41
+ return '0.0.0';
42
+ }
43
+
44
+ const CLI_VERSION = readCliVersion();
8
45
 
9
46
  function buildHelpFooter(spec) {
10
47
  const lines = [
@@ -44,19 +81,70 @@ function ensureParentCommand(program, parentMap, parentPath) {
44
81
  }
45
82
 
46
83
  function buildRuntime(globalOptions, spec) {
47
- const runtime = resolveRuntimeProfile(globalOptions, { requireAuth: spec.require_auth !== false });
84
+ const localBackendTypes = new Set(['local', 'local_config', 'local_registry']);
85
+ const commandName = spec.command_path.join(' ');
86
+ const liveAppHarness = (
87
+ commandName === 'apps verify' || commandName === 'apps screenshot'
88
+ ) && globalOptions.mode === 'live';
89
+ const runtime = resolveRuntimeProfile(globalOptions, {
90
+ requireAuth: spec.require_auth !== false,
91
+ allowUnknownProfile: spec.allow_unknown_profile === true,
92
+ allowUnavailableWorktree:
93
+ localBackendTypes.has(spec.backend_call?.type) && !liveAppHarness,
94
+ });
95
+ // A stopped local-only worktree may keep executing commands whose backend is
96
+ // entirely local, but it must not carry a shared live credential into those
97
+ // handlers or their post-command telemetry. Naming --profile explicitly is
98
+ // the deliberate escape hatch from the worktree boundary.
99
+ if (runtime.worktreeRuntimeUnavailable && runtime.profileSource !== 'explicit') {
100
+ runtime.jwt = undefined;
101
+ runtime.credentialKind = undefined;
102
+ runtime.credentialSource = undefined;
103
+ runtime.oauthAccessToken = undefined;
104
+ runtime.oauthRefreshToken = undefined;
105
+ }
48
106
  return {
49
107
  ...runtime,
50
108
  cliVersion: CLI_VERSION,
51
109
  color: globalOptions.color !== false,
110
+ quiet: Boolean(globalOptions.quiet),
111
+ verbose: Boolean(globalOptions.verbose),
52
112
  workspacePath,
53
113
  };
54
114
  }
55
115
 
56
- function attachSpec(program, parentMap, spec, specs) {
116
+ function buildErrorRuntime(globalOptions) {
117
+ try {
118
+ return {
119
+ ...resolveRuntimeProfile(globalOptions, {
120
+ requireAuth: false,
121
+ includeDebugEntitlementOverride: false,
122
+ allowUnknownProfile: true,
123
+ }),
124
+ cliVersion: CLI_VERSION,
125
+ color: globalOptions.color !== false,
126
+ quiet: Boolean(globalOptions.quiet),
127
+ verbose: Boolean(globalOptions.verbose),
128
+ workspacePath,
129
+ };
130
+ } catch {
131
+ return {
132
+ profileName: globalOptions.profile || DEFAULT_PROFILE,
133
+ apiBase: null,
134
+ outputMode: resolveOutputMode(globalOptions),
135
+ cliVersion: CLI_VERSION,
136
+ color: globalOptions.color !== false,
137
+ quiet: Boolean(globalOptions.quiet),
138
+ verbose: Boolean(globalOptions.verbose),
139
+ workspacePath,
140
+ };
141
+ }
142
+ }
143
+
144
+ function attachSpec(program, parentMap, spec, specs, launchContext = {}) {
57
145
  const parent = ensureParentCommand(program, parentMap, spec.command_path.slice(0, -1));
58
146
  const leaf = spec.command_path[spec.command_path.length - 1];
59
- const command = parent.command(leaf).description(spec.summary);
147
+ const command = parent.command(leaf, spec.hidden ? { hidden: true } : undefined).description(spec.summary);
60
148
  command.addHelpText('after', buildHelpFooter(spec));
61
149
 
62
150
  for (const argument of spec.args_schema?.arguments || []) {
@@ -64,10 +152,20 @@ function attachSpec(program, parentMap, spec, specs) {
64
152
  }
65
153
 
66
154
  for (const option of spec.args_schema?.options || []) {
67
- command.option(option.flags, option.description);
155
+ if (option.collect) {
156
+ command.option(
157
+ option.flags,
158
+ option.description,
159
+ (value, previous) => [...(previous || []), value],
160
+ [],
161
+ );
162
+ } else {
163
+ command.option(option.flags, option.description);
164
+ }
68
165
  }
69
166
 
70
167
  command.action(async (...raw) => {
168
+ const startedAt = Date.now();
71
169
  const commanderCommand = raw[raw.length - 1];
72
170
  const argumentValues = raw.slice(0, -1);
73
171
  const globalOptions = commanderCommand.optsWithGlobals();
@@ -88,17 +186,30 @@ function attachSpec(program, parentMap, spec, specs) {
88
186
  globalOptions,
89
187
  runtime,
90
188
  output,
189
+ ...launchContext,
91
190
  });
92
191
  process.exitCode = typeof exitCode === 'number' ? exitCode : 0;
192
+ if (process.exitCode === 0 && !['logout', 'agent-context', 'agent-capture'].includes(spec.command_path[0])) {
193
+ await maybeInstallSkillSyncService(runtime).catch(() => undefined);
194
+ }
195
+ await reportCliCommand({
196
+ spec,
197
+ runtime,
198
+ result: process.exitCode === 0 ? 'success' : 'failed',
199
+ durationMs: Date.now() - startedAt,
200
+ error: process.exitCode === 0 ? null : { exitCode: process.exitCode },
201
+ });
93
202
  } catch (error) {
94
- const runtime = {
95
- ...resolveRuntimeProfile(globalOptions, { requireAuth: false }),
96
- cliVersion: CLI_VERSION,
97
- color: globalOptions.color !== false,
98
- workspacePath,
99
- };
203
+ const runtime = buildErrorRuntime(globalOptions);
100
204
  const output = new OutputManager(runtime);
101
205
  const cliError = asCliError(error);
206
+ await reportCliCommand({
207
+ spec,
208
+ runtime,
209
+ result: 'failed',
210
+ durationMs: Date.now() - startedAt,
211
+ error: cliError,
212
+ });
102
213
  process.exitCode = output.emitError({
103
214
  command: spec.command_path.join(' '),
104
215
  error: cliError,
@@ -107,12 +218,12 @@ function attachSpec(program, parentMap, spec, specs) {
107
218
  });
108
219
  }
109
220
 
110
- export function createProgram() {
221
+ export function createProgram(launchContext = {}) {
111
222
  const program = new Command();
112
223
 
113
224
  program
114
225
  .name('notis')
115
- .description('Agent-first Notis CLI for apps, databases, and generic tool execution')
226
+ .description('Agent-first Notis CLI for apps and generic tool execution')
116
227
  .version(CLI_VERSION)
117
228
  .showHelpAfterError()
118
229
  .option('--json', 'Shortcut for --output json')
@@ -121,27 +232,185 @@ export function createProgram() {
121
232
  .option('--quiet', 'Suppress non-essential human output')
122
233
  .option('--verbose', 'Show extra human-readable diagnostics')
123
234
  .option('--no-color', 'Disable ANSI color output')
124
- .option('--profile <name>', 'CLI profile name', 'default')
235
+ // No default value: commander cannot tell a default apart from an explicit
236
+ // flag, and a defaulted "default" here silently overrode every
237
+ // `notis profile use`, pinning all commands to the default profile.
238
+ .option('--profile <name>', 'CLI profile to run as (defaults to the active profile)')
125
239
  .option('--api-base <url>', 'Override the API base URL for this invocation')
126
240
  .option('--timeout-ms <n>', 'HTTP timeout in milliseconds')
127
- .option('--idempotency-key <key>', 'Override the generated idempotency key for mutating commands');
241
+ .option('--idempotency-key <key>', 'Override the generated idempotency key for mutating commands')
242
+ .addOption(new Option('--notis-managed-agent-hook').hideHelp());
128
243
 
129
244
  const parentMap = new Map([['', program]]);
130
245
  for (const spec of COMMAND_SPECS) {
131
- attachSpec(program, parentMap, spec, COMMAND_SPECS);
246
+ attachSpec(program, parentMap, spec, COMMAND_SPECS, launchContext);
132
247
  }
133
248
 
134
249
  return program;
135
250
  }
136
251
 
137
- export async function run(argv = process.argv) {
138
- const program = createProgram();
139
- await program.parseAsync(argv);
252
+ /**
253
+ * Read the two global flags that decide which build should serve this run.
254
+ *
255
+ * Commander cannot help here: the decision has to be made before the program
256
+ * parses, because the answer may be to hand the whole invocation to a
257
+ * different process. Only `--profile` and `--api-base` matter, and both are
258
+ * plain `--flag value` pairs.
259
+ */
260
+ export function readChannelRelevantFlags(args = []) {
261
+ const flags = {};
262
+ for (const [index, token] of args.entries()) {
263
+ for (const [flag, key] of [['--profile', 'profile'], ['--api-base', 'apiBase']]) {
264
+ if (token === flag) flags[key] = args[index + 1];
265
+ else if (token.startsWith(`${flag}=`)) flags[key] = token.slice(flag.length + 1);
266
+ }
267
+ }
268
+ return flags;
140
269
  }
141
270
 
142
- const isDirectInvocation =
143
- process.argv[1] && import.meta.url === new URL(process.argv[1], 'file://').href;
271
+ export function channelProfileForArgs(
272
+ args = [],
273
+ env = process.env,
274
+ { config: suppliedConfig, worktreeRuntime: suppliedWorktreeRuntime } = {},
275
+ ) {
276
+ // An explicit endpoint overrides the stored profile for this run, so it also
277
+ // decides the build: `--api-base https://api-beta.notis.ai` on a production
278
+ // profile is a deliberate one-off beta call.
279
+ const { profile: profileName, apiBase } = readChannelRelevantFlags(args);
280
+ if (apiBase) {
281
+ return { api_base: apiBase };
282
+ }
283
+ if (env.NOTIS_API_BASE) {
284
+ return { api_base: env.NOTIS_API_BASE };
285
+ }
286
+ const config = suppliedConfig || loadConfig();
287
+ const explicitProfile = profileName || env.NOTIS_PROFILE;
288
+ if (explicitProfile) {
289
+ return getProfile(config, explicitProfile);
290
+ }
291
+ const resolvedWorktree = suppliedWorktreeRuntime === undefined
292
+ ? resolveWorktreeRuntime()
293
+ : suppliedWorktreeRuntime;
294
+ // A stopped local-only worktree must reach the normal routing error on the
295
+ // current build. Switching based on an unrelated shared profile would escape
296
+ // the worktree boundary before that fail-closed check runs.
297
+ if (resolvedWorktree?.unavailable) {
298
+ return {};
299
+ }
300
+ if (resolvedWorktree?.profile) {
301
+ return resolvedWorktree;
302
+ }
303
+ // NOTIS_JWT replaces the credential, not the route. Unless NOTIS_API_BASE
304
+ // was explicit above, the selected/current profile still owns the channel.
305
+ return getProfile(config, config.current_profile || DEFAULT_PROFILE);
306
+ }
307
+
308
+ function interruptedExitCode(signal) {
309
+ if (signal === 'SIGINT') return 130;
310
+ if (signal === 'SIGTERM') return 143;
311
+ return 1;
312
+ }
313
+
314
+ export function executeChannelSwitch(
315
+ decision,
316
+ args,
317
+ env,
318
+ { spawn = spawnSync } = {},
319
+ ) {
320
+ const childEnv = { ...env, [CHANNEL_SWITCH_ENV]: '1' };
321
+ // First make npm resolve and boot the target package without executing the
322
+ // requested command. That distinguishes an unavailable tag/network from a
323
+ // legitimate non-zero exit of the handed-off CLI, which must be propagated
324
+ // rather than retried locally after a possible mutation.
325
+ const probe = spawn(decision.command, [...decision.args, '--version'], {
326
+ stdio: 'ignore',
327
+ env: childEnv,
328
+ });
329
+ if (probe.signal) {
330
+ return {
331
+ ...decision,
332
+ exitCode: interruptedExitCode(probe.signal),
333
+ reason: 'switch_interrupted',
334
+ };
335
+ }
336
+ if (probe.error || probe.status !== 0) {
337
+ return { ...decision, switch: false, reason: 'switch_unavailable' };
338
+ }
339
+
340
+ const result = spawn(decision.command, [...decision.args, ...args], {
341
+ stdio: 'inherit',
342
+ env: childEnv,
343
+ });
344
+ if (result.signal) {
345
+ return {
346
+ ...decision,
347
+ exitCode: interruptedExitCode(result.signal),
348
+ reason: 'switch_interrupted',
349
+ };
350
+ }
351
+ if (result.error || typeof result.status !== 'number') {
352
+ return { ...decision, switch: false, reason: 'switch_failed' };
353
+ }
354
+ return { ...decision, exitCode: result.status };
355
+ }
144
356
 
145
- if (isDirectInvocation) {
146
- run();
357
+ /**
358
+ * Hand this invocation to the published build the profile is pinned to.
359
+ *
360
+ * Returns the decision rather than exiting so tests can assert on it. Any
361
+ * failure to reach the other build is non-fatal: running the wrong channel is
362
+ * a much smaller problem than refusing to run at all.
363
+ */
364
+ export function switchChannelIfNeeded(
365
+ argv = process.argv,
366
+ env = process.env,
367
+ { spawn = spawnSync, platform = process.platform, moduleDirectory } = {},
368
+ ) {
369
+ const args = argv.slice(2);
370
+ let profile;
371
+ try {
372
+ profile = channelProfileForArgs(args, env);
373
+ } catch {
374
+ return { switch: false, reason: 'profile_unreadable' };
375
+ }
376
+ const decision = resolveChannelSwitch({
377
+ runningVersion: CLI_VERSION,
378
+ profile,
379
+ moduleDirectory: moduleDirectory || dirname(fileURLToPath(import.meta.url)),
380
+ env,
381
+ platform,
382
+ });
383
+ if (!decision.switch) {
384
+ return decision;
385
+ }
386
+
387
+ const result = executeChannelSwitch(decision, args, env, { spawn });
388
+ if (!result.switch) {
389
+ process.stderr.write(
390
+ `Notis CLI could not start the ${decision.targetChannel} build for this profile; `
391
+ + `continuing on ${decision.runningChannel}.\n`,
392
+ );
393
+ }
394
+ return result;
395
+ }
396
+
397
+ export function isSkillsSyncInvocation(args) {
398
+ return args.some((value, index) => value === 'skills' && args[index + 1] === 'sync');
399
+ }
400
+
401
+ export async function run(argv = process.argv) {
402
+ const switched = switchChannelIfNeeded(argv);
403
+ if (switched.switch) {
404
+ process.exitCode = switched.exitCode;
405
+ return;
406
+ }
407
+ const preexistingAgentIds = detectedAgentIds();
408
+ // Every actual CLI launch repairs the three system skills before doing any
409
+ // command work, including before an authenticated command can fail. Capture
410
+ // vendor presence first because reconciliation creates their skill roots.
411
+ const basePreflightResult = await withSkillSyncLock(
412
+ async () => reconcileBaseSkillsBestEffort(),
413
+ ).catch(() => undefined);
414
+ const program = createProgram({ preexistingAgentIds, basePreflightResult });
415
+ await program.parseAsync(argv);
147
416
  }