@guiho/runx 0.2.6 → 0.4.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/DOCS.md +128 -109
  3. package/README.md +58 -58
  4. package/devops/build-binaries.ts +44 -62
  5. package/devops/devops.xdocs.md +8 -5
  6. package/devops/install.ps1 +182 -152
  7. package/devops/install.sh +197 -262
  8. package/devops/installers.spec.ts +31 -0
  9. package/devops/verify-release-assets.ts +17 -0
  10. package/docs/decisions/decisions.xdocs.md +4 -0
  11. package/docs/decisions/interactive-init-manifest.md +187 -0
  12. package/docs/docs.xdocs.md +3 -1
  13. package/docs/plans/interactive-init-manifest.md +177 -0
  14. package/docs/plans/plans.xdocs.md +11 -0
  15. package/docs/plans/rfc-0034-cli-compliance-migration.md +586 -0
  16. package/docs/plans/upgrade-reliability-implementation.md +95 -0
  17. package/docs/reviews/implementation/implementation.xdocs.md +6 -0
  18. package/docs/reviews/implementation/interactive-init-manifest-review.md +69 -0
  19. package/docs/reviews/implementation/rfc-0034-cli-compliance-migration-review.md +59 -0
  20. package/docs/reviews/plans/interactive-init-manifest-review.md +82 -0
  21. package/docs/reviews/plans/plans.xdocs.md +10 -0
  22. package/docs/reviews/plans/rfc-0034-cli-compliance-migration-review.md +80 -0
  23. package/docs/reviews/plans/upgrade-reliability-implementation-review.md +65 -0
  24. package/docs/superpowers/specs/2026-07-15-upgrade-reliability-design.md +662 -0
  25. package/docs/superpowers/specs/specs.xdocs.md +24 -0
  26. package/docs/superpowers/superpowers.xdocs.md +21 -0
  27. package/docs/todo/rfc-0034-cli-compliance-migration-implementation.md +58 -0
  28. package/docs/todo/rfc-0034-cli-compliance-migration.md +151 -0
  29. package/docs/todo/todo.xdocs.md +6 -2
  30. package/docs/validation/interactive-init-manifest.md +79 -0
  31. package/docs/validation/rfc-0034-cli-compliance-migration.md +67 -0
  32. package/docs/validation/upgrade-reliability.md +124 -0
  33. package/docs/validation/validation.xdocs.md +9 -0
  34. package/library/agents.d.ts +28 -4
  35. package/library/agents.d.ts.map +1 -1
  36. package/library/agents.js +143 -41
  37. package/library/cli.d.ts.map +1 -1
  38. package/library/cli.js +297 -317
  39. package/library/configuration.d.ts +57 -0
  40. package/library/configuration.d.ts.map +1 -0
  41. package/library/configuration.js +111 -0
  42. package/library/embedded-resources.d.ts +6 -1
  43. package/library/embedded-resources.d.ts.map +1 -1
  44. package/library/embedded-resources.js +10 -4
  45. package/library/executor.d.ts +5 -1
  46. package/library/executor.d.ts.map +1 -1
  47. package/library/executor.js +10 -10
  48. package/library/help.d.ts +8 -4
  49. package/library/help.d.ts.map +1 -1
  50. package/library/help.js +70 -42
  51. package/library/init.d.ts +18 -0
  52. package/library/init.d.ts.map +1 -0
  53. package/library/init.js +43 -0
  54. package/library/manifest.d.ts +4 -40
  55. package/library/manifest.d.ts.map +1 -1
  56. package/library/manifest.js +4 -112
  57. package/library/path-utils.d.ts +13 -0
  58. package/library/path-utils.d.ts.map +1 -0
  59. package/library/path-utils.js +82 -0
  60. package/library/recovery.d.ts +7 -0
  61. package/library/recovery.d.ts.map +1 -0
  62. package/library/recovery.js +23 -0
  63. package/library/release-catalog.d.ts +32 -0
  64. package/library/release-catalog.d.ts.map +1 -0
  65. package/library/release-catalog.js +124 -0
  66. package/library/self-management.d.ts +24 -4
  67. package/library/self-management.d.ts.map +1 -1
  68. package/library/self-management.js +279 -99
  69. package/library/storage.d.ts +13 -0
  70. package/library/storage.d.ts.map +1 -0
  71. package/library/storage.js +38 -0
  72. package/library/types.d.ts +11 -16
  73. package/library/types.d.ts.map +1 -1
  74. package/library/types.js +3 -0
  75. package/library/update-cache.d.ts +21 -0
  76. package/library/update-cache.d.ts.map +1 -0
  77. package/library/update-cache.js +68 -0
  78. package/library/upgrade-reporting.d.ts +11 -0
  79. package/library/upgrade-reporting.d.ts.map +1 -0
  80. package/library/upgrade-reporting.js +67 -0
  81. package/library/upgrade-types.d.ts +72 -0
  82. package/library/upgrade-types.d.ts.map +1 -0
  83. package/library/upgrade-types.js +3 -0
  84. package/package.json +6 -3
  85. package/scripts/runx-bin.mjs +49 -0
  86. package/scripts/runx-bin.spec.ts +62 -0
  87. package/scripts/scripts.xdocs.md +3 -3
  88. package/skills/guiho-s-runx/SKILL.md +64 -59
  89. package/skills/guiho-s-runx/guiho-s-runx.xdocs.md +7 -4
  90. package/skills/skills.xdocs.md +1 -1
  91. package/docs/todo/implement-runx-alpha.md +0 -36
  92. package/scripts/runx-bin.ts +0 -24
package/library/cli.js CHANGED
@@ -1,15 +1,21 @@
1
1
  /**
2
2
  * @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
3
  */
4
- import { resolve } from 'node:path';
4
+ import { Type } from '@sinclair/typebox';
5
+ import { Value } from '@sinclair/typebox/value';
5
6
  import { defineCommand, renderUsage, runCommand as runCittyCommand } from 'citty';
6
- import { installAgentInstructions, installAgentSkill } from './agents.js';
7
+ import { applyAgentInstructions, installAgentSkill, listAgentPrompts, listAgentSkills, removeAgentInstructions, showAgentInstructions, showAgentPrompt, showAgentSkill, uninstallAgentSkill, updateAgentInstructions, updateAgentSkill, } from './agents.js';
7
8
  import { RunXError } from './errors.js';
8
9
  import { runCommand } from './executor.js';
9
- import { readVersion, showHelpDocs, showHelpTree, showHome } from './help.js';
10
+ import { readVersion, renderHelpDocs, renderHelpTree } from './help.js';
11
+ import { initializeRunXManifest } from './init.js';
10
12
  import { readManifest, resolveCommand } from './manifest.js';
13
+ import { resolvePath } from './path-utils.js';
11
14
  import { renderDescription, renderExecutionPlan, renderJson, renderList } from './render.js';
12
- import { checkForLatestVersion, listAvailableVersions, uninstallSelf, upgradeSelf } from './self-management.js';
15
+ import { fetchReleaseCatalog, resolveUpgradePlatform } from './release-catalog.js';
16
+ import { checkForLatestVersion, uninstallSelf, upgradeSelf } from './self-management.js';
17
+ import { readCachedUpdateNotice, runUpdateWorker, spawnUpdateWorker } from './update-cache.js';
18
+ import { renderReleaseCatalog, renderUpgradeEvent, renderUpgradeHeading, renderUpgradePlan, renderUpgradeResult } from './upgrade-reporting.js';
13
19
  export { runCli, runCliWithErrorHandling, runxCommand, };
14
20
  class CliHandled extends Error {
15
21
  }
@@ -21,199 +27,202 @@ class CliUsageError extends Error {
21
27
  this.usage = usage;
22
28
  }
23
29
  }
24
- const commonArgs = {
25
- cwd: { type: 'string', description: 'Resolve the manifest from this directory.', valueHint: 'path' },
26
- file: { type: 'string', description: 'Use this runx.yaml file.', valueHint: 'path' },
27
- format: { type: 'string', description: 'Select text or JSON output.', valueHint: 'text|json', default: 'text' },
28
- verbose: { type: 'boolean', description: 'Enable additional diagnostics.' },
29
- 'help-tree': { type: 'boolean', description: 'Show the complete command hierarchy.' },
30
- 'help-docs': { type: 'boolean', description: 'Show manifest and agent guidance.' },
30
+ const formatSchema = Type.Union([Type.Literal('text'), Type.Literal('json')]);
31
+ const archSchema = Type.Union([Type.Literal('x64'), Type.Literal('arm64')]);
32
+ const variantSchema = Type.Union([Type.Literal('baseline'), Type.Literal('default'), Type.Literal('modern')]);
33
+ const positiveIntegerSchema = Type.Integer({ minimum: 1 });
34
+ const helpArgs = {
31
35
  help: { type: 'boolean', alias: 'h', description: 'Show command help.' },
32
- version: { type: 'boolean', alias: 'v', description: 'Show the RunX version.' },
36
+ 'help-tree': { type: 'boolean', description: 'Show this command hierarchy.' },
37
+ 'help-tree-depth': { type: 'string', valueHint: 'positive-integer', description: 'Limit help-tree recursion depth.' },
38
+ 'help-docs': { type: 'boolean', description: 'Emit Markdown documentation for this command.' },
33
39
  };
34
- const runArgs = {
35
- ...commonArgs,
36
- 'dry-run': { type: 'boolean', description: 'Show the action without executing it.' },
37
- yes: { type: 'boolean', description: 'Approve a confirmation-gated command.' },
40
+ const catalogArgs = {
41
+ cwd: { type: 'string', valueHint: 'path', description: 'Use this effective working directory.' },
42
+ config: { type: 'string', valueHint: 'path', description: 'Use this runx.yaml configuration file.' },
43
+ format: { type: 'string', valueHint: 'text|json', default: 'text', description: 'Select output format.' },
44
+ verbose: { type: 'boolean', description: 'Enable diagnostics.' },
45
+ ...helpArgs,
38
46
  };
39
- const agentInstallArgs = {
40
- ...commonArgs,
41
- tool: { type: 'string', description: 'Select agents, claude, or all.', valueHint: 'agents|claude|all' },
42
- };
43
- const selfManagementArgs = {
44
- ...commonArgs,
45
- 'dry-run': { type: 'boolean', description: 'Show the action without changing the executable.' },
46
- };
47
- const rootArgs = {
48
- ...runArgs,
49
- tool: agentInstallArgs.tool,
50
- };
51
- const knownArgumentKeys = new Set([
52
- '_',
53
- 'cwd',
54
- 'file',
55
- 'format',
56
- 'verbose',
57
- 'dryRun',
58
- 'dry-run',
59
- 'yes',
60
- 'tool',
61
- 'helpTree',
62
- 'help-tree',
63
- 'helpDocs',
64
- 'help-docs',
65
- 'help',
66
- 'h',
67
- 'version',
68
- 'v',
69
- ]);
70
- const createCommandTree = () => {
71
- const state = { globalArgs: { _: [] }, usageCommand: {} };
72
- const listCommand = defineCommand({
73
- meta: { name: 'runx list', description: 'List commands in a RunX manifest.' },
74
- args: commonArgs,
75
- setup: withCommandHelp(state),
76
- run: async () => listCommands(resolveOptions(state.globalArgs)),
77
- });
78
- const describeCommandDefinition = defineCommand({
79
- meta: { name: 'runx describe', description: 'Describe one manifest command.' },
80
- args: {
81
- selector: { type: 'positional', description: 'Command UID, group/id, index, or unambiguous ID.' },
82
- ...commonArgs,
83
- },
84
- setup: withCommandHelp(state),
85
- run: async ({ args }) => {
86
- if (!args.selector)
87
- throw await missingSelector(state);
88
- await describeCommand(args.selector, resolveOptions(state.globalArgs));
89
- },
90
- });
91
- const runCommandDefinition = createRunCommand(state);
92
- const checkCommand = defineCommand({
93
- meta: { name: 'runx check', description: 'Validate a RunX manifest without executing it.' },
94
- args: commonArgs,
95
- setup: withCommandHelp(state),
96
- run: async () => checkManifest(resolveOptions(state.globalArgs)),
97
- });
98
- const agentsInstallCommand = defineCommand({
99
- meta: { name: 'runx agents install', description: 'Install the bundled RunX skill.' },
100
- args: {
101
- scope: { type: 'positional', description: 'Install locally or globally.', default: 'local', valueHint: 'local|global' },
102
- ...agentInstallArgs,
103
- },
104
- setup: withCommandHelp(state),
105
- run: async ({ args }) => runAgentsInstall(args.scope, resolveOptions(state.globalArgs), state.globalArgs),
106
- });
107
- const agentsInstructionsCommand = defineCommand({
108
- meta: { name: 'runx agents instructions', description: 'Install or refresh managed RunX agent instructions.' },
109
- args: commonArgs,
110
- setup: withCommandHelp(state),
111
- run: async () => runAgentsInstructions(resolveOptions(state.globalArgs)),
112
- });
113
- const agentsCommand = defineCommand({
114
- meta: { name: 'runx agents', description: 'Manage the bundled RunX agent integration.' },
115
- args: agentInstallArgs,
116
- subCommands: {
117
- install: agentsInstallCommand,
118
- instructions: agentsInstructionsCommand,
119
- },
120
- setup: withCommandHelp(state),
47
+ function createCommandTree() {
48
+ const state = { args: { _: [] }, command: {}, commands: new Map() };
49
+ const leaf = (name, description, args, run) => {
50
+ const command = defineCommand({ meta: { name, description }, args: { ...args, ...helpArgs }, setup: helpSetup(state), run });
51
+ state.commands.set(name.replace(/^runx(?:\s+|$)/, ''), command);
52
+ return command;
53
+ };
54
+ const list = leaf('runx list', 'List commands in a RunX configuration.', catalogArgs, async () => listCommands(options(state.args)));
55
+ const describe = leaf('runx describe', 'Describe one catalog command without execution.', {
56
+ selector: { type: 'positional', description: 'UID, group/id, index, or unambiguous ID.' },
57
+ ...catalogArgs,
58
+ }, async ({ args }) => {
59
+ if (!args.selector)
60
+ throw await usageError(state, 'Missing required positional argument: SELECTOR');
61
+ await describeCommand(String(args.selector), options(state.args));
121
62
  });
122
- const upgradeApplyCommand = defineCommand({
123
- meta: { name: 'runx upgrade', description: 'Upgrade a native RunX executable.', hidden: true },
124
- args: selfManagementArgs,
125
- setup: withCommandHelp(state),
126
- run: async () => runUpgrade(resolveOptions(state.globalArgs), state.globalArgs),
63
+ const run = leaf('runx run', 'Execute one selected catalog command.', {
64
+ selector: { type: 'positional', description: 'UID, group/id, index, or unambiguous ID.' },
65
+ ...catalogArgs,
66
+ 'dry-run': { type: 'boolean', description: 'Print the execution plan without spawning.' },
67
+ yes: { type: 'boolean', description: 'Approve a confirmation-gated command.' },
68
+ }, async ({ args }) => {
69
+ if (!args.selector)
70
+ throw await usageError(state, 'Missing required positional argument: SELECTOR');
71
+ await runSelected(String(args.selector), options(state.args), state.args);
127
72
  });
128
- const upgradeCheckCommand = defineCommand({
129
- meta: { name: 'runx upgrade check', description: 'Check whether a newer RunX release is available.' },
130
- args: commonArgs,
131
- setup: withCommandHelp(state),
132
- run: async () => runUpgradeCheck(resolveOptions(state.globalArgs)),
73
+ const check = leaf('runx check', 'Validate a RunX configuration without execution.', catalogArgs, async () => checkManifest(options(state.args)));
74
+ const init = leaf('runx init', 'Create a new YAML RunX configuration.', catalogArgs, async () => {
75
+ const result = await initializeRunXManifest({ cwd: options(state.args).cwd, config: options(state.args).config });
76
+ write(state.args.format === 'json' ? renderJson(result) : `created: ${result.path}\n`);
133
77
  });
134
- const upgradeListCommand = defineCommand({
135
- meta: { name: 'runx upgrade list', description: 'List recent RunX release versions.' },
136
- args: commonArgs,
137
- setup: withCommandHelp(state),
138
- run: async () => runUpgradeList(resolveOptions(state.globalArgs)),
78
+ const skillInstall = leaf('runx agent skill install', 'Install the bundled skill into both global tool locations.', {
79
+ cwd: catalogArgs.cwd, local: { type: 'boolean', description: 'Use project-local tool directories.' }, format: catalogArgs.format,
80
+ }, async () => agentMutation('installed', installAgentSkill, state.args));
81
+ const skillUninstall = leaf('runx agent skill uninstall', 'Remove the bundled skill from both tool locations.', {
82
+ cwd: catalogArgs.cwd, local: { type: 'boolean', description: 'Use project-local tool directories.' }, format: catalogArgs.format,
83
+ }, async () => agentMutation('removed', uninstallAgentSkill, state.args));
84
+ const skillUpdate = leaf('runx agent skill update', 'Refresh the bundled skill in both tool locations.', {
85
+ cwd: catalogArgs.cwd, local: { type: 'boolean', description: 'Use project-local tool directories.' }, format: catalogArgs.format,
86
+ }, async () => agentMutation('updated', updateAgentSkill, state.args));
87
+ const skillList = leaf('runx agent skill list', 'List bundled RunX skills.', {
88
+ filter: { type: 'string', valueHint: 'keyword', description: 'Filter skill metadata.' }, format: catalogArgs.format,
89
+ }, () => writeFormatted(listAgentSkills(state.args.filter), state.args));
90
+ const skillShow = leaf('runx agent skill show', 'Show metadata for one bundled skill.', {
91
+ id: { type: 'positional', description: 'Bundled skill ID.' }, format: catalogArgs.format,
92
+ }, async ({ args }) => {
93
+ if (!args.id)
94
+ throw await usageError(state, 'Missing required positional argument: ID');
95
+ writeFormatted(await showAgentSkill(String(args.id)), state.args);
139
96
  });
140
- const upgradeCommand = defineCommand({
141
- meta: { name: 'runx upgrade', description: 'Inspect or upgrade a native RunX executable.' },
142
- args: selfManagementArgs,
143
- default: '_apply',
144
- subCommands: {
145
- _apply: upgradeApplyCommand,
146
- check: upgradeCheckCommand,
147
- list: upgradeListCommand,
148
- },
149
- setup: withCommandHelp(state),
97
+ const skill = group('runx agent skill', 'Manage the bundled RunX skill.', { install: skillInstall, uninstall: skillUninstall, update: skillUpdate, list: skillList, show: skillShow }, state);
98
+ const instructionApply = leaf('runx agent instruction apply', 'Apply the managed instruction block.', { cwd: catalogArgs.cwd, format: catalogArgs.format }, async () => instructionMutation('updated', applyAgentInstructions, state.args));
99
+ const instructionRemove = leaf('runx agent instruction remove', 'Remove the managed instruction block.', { cwd: catalogArgs.cwd, format: catalogArgs.format }, async () => instructionMutation('removed', removeAgentInstructions, state.args));
100
+ const instructionUpdate = leaf('runx agent instruction update', 'Replace stale managed instruction content.', { cwd: catalogArgs.cwd, format: catalogArgs.format }, async () => instructionMutation('updated', updateAgentInstructions, state.args));
101
+ const instructionShow = leaf('runx agent instruction show', 'Print the raw instruction template.', {}, async () => write(await showAgentInstructions()));
102
+ const instruction = group('runx agent instruction', 'Manage RunX instruction blocks.', { apply: instructionApply, remove: instructionRemove, update: instructionUpdate, show: instructionShow }, state);
103
+ const promptList = leaf('runx agent prompt list', 'List bundled RunX prompts.', {
104
+ names: { type: 'boolean', description: 'Print prompt names only.' }, format: catalogArgs.format,
105
+ }, () => writeFormatted(listAgentPrompts(Boolean(state.args.names)), state.args));
106
+ const promptShow = leaf('runx agent prompt show', 'Print one raw bundled prompt.', {
107
+ id: { type: 'positional', description: 'Bundled prompt ID.' },
108
+ }, async ({ args }) => {
109
+ if (!args.id)
110
+ throw await usageError(state, 'Missing required positional argument: ID');
111
+ write(await showAgentPrompt(String(args.id)));
150
112
  });
151
- const uninstallCommand = defineCommand({
152
- meta: { name: 'runx uninstall', description: 'Uninstall a native RunX executable.' },
153
- args: selfManagementArgs,
154
- setup: withCommandHelp(state),
155
- run: async () => runUninstall(resolveOptions(state.globalArgs), state.globalArgs),
113
+ const prompt = group('runx agent prompt', 'Inspect bundled agent prompts.', { list: promptList, show: promptShow }, state);
114
+ const agent = group('runx agent', 'Manage RunX agent integration.', { skill, instruction, prompt }, state);
115
+ const upgradeApply = leaf('runx upgrade', 'Upgrade the canonical native RunX executable.', {
116
+ version: { type: 'string', valueHint: 'version', description: 'Select an exact release version.' },
117
+ arch: { type: 'string', valueHint: 'x64|arm64', description: 'Select target architecture.' },
118
+ variant: { type: 'string', valueHint: 'baseline|default|modern', description: 'Select x64 binary variant.' },
119
+ 'dry-run': { type: 'boolean', description: 'Plan without mutation.' },
120
+ format: catalogArgs.format,
121
+ }, async () => runUpgrade(options(state.args), state.args));
122
+ upgradeApply.meta = { ...upgradeApply.meta, hidden: true };
123
+ const upgradeCheck = leaf('runx upgrade check', 'Check whether a newer stable release exists.', {
124
+ format: catalogArgs.format,
125
+ }, async () => {
126
+ const result = await checkForLatestVersion();
127
+ writeFormatted(result, state.args);
156
128
  });
157
- const homeCommand = defineCommand({
158
- meta: { name: 'runx', description: 'Show the RunX home page.', hidden: true },
159
- args: rootArgs,
160
- setup: withCommandHelp(state),
161
- run: () => write(showHome()),
129
+ const upgradeList = leaf('runx upgrade list', 'List RunX releases newest first.', {
130
+ page: { type: 'string', valueHint: 'positive-integer', description: 'Select result page.' },
131
+ 'per-page': { type: 'string', valueHint: 'positive-integer', description: 'Select page size.' },
132
+ 'pre-releases': { type: 'boolean', description: 'Include prerelease versions.' },
133
+ arch: { type: 'string', valueHint: 'x64|arm64', description: 'Select target architecture.' },
134
+ variant: { type: 'string', valueHint: 'baseline|default|modern', description: 'Select x64 variant.' },
135
+ format: catalogArgs.format,
136
+ }, async () => runUpgradeList(state.args));
137
+ const upgrade = group('runx upgrade', 'Inspect or upgrade a native RunX executable.', { _apply: upgradeApply, check: upgradeCheck, list: upgradeList }, state, '_apply');
138
+ const uninstall = leaf('runx uninstall', 'Uninstall the native RunX executable.', {
139
+ 'dry-run': { type: 'boolean', description: 'Print the target without deleting it.' }, format: catalogArgs.format,
140
+ }, async () => {
141
+ const result = await uninstallSelf(Boolean(state.args.dryRun));
142
+ writeFormatted(result, state.args);
162
143
  });
163
- const staticSubCommands = Object.assign(Object.create(null), {
164
- _home: homeCommand,
165
- list: listCommand,
166
- describe: describeCommandDefinition,
167
- run: runCommandDefinition,
168
- check: checkCommand,
169
- agents: agentsCommand,
170
- upgrade: upgradeCommand,
171
- uninstall: uninstallCommand,
172
- });
173
- const usageCommands = new Map([
174
- ['list', listCommand],
175
- ['describe', describeCommandDefinition],
176
- ['run', runCommandDefinition],
177
- ['r', runCommandDefinition],
178
- ['check', checkCommand],
179
- ['agents', agentsCommand],
180
- ['agents install', agentsInstallCommand],
181
- ['agents instructions', agentsInstructionsCommand],
182
- ['upgrade', upgradeCommand],
183
- ['upgrade check', upgradeCheckCommand],
184
- ['upgrade list', upgradeListCommand],
185
- ['uninstall', uninstallCommand],
186
- ]);
187
- const subCommands = createSelectorCompatibleCommands(staticSubCommands, state);
188
- const command = defineCommand({
189
- meta: {
190
- name: 'runx',
191
- version: readVersion(),
192
- description: 'A documented, local command catalog for runx.yaml manifests.',
193
- },
194
- args: rootArgs,
195
- default: '_home',
196
- subCommands,
144
+ const root = defineCommand({
145
+ meta: { name: 'runx', version: readVersion(), description: 'A documented local command catalog for runx.yaml.' },
146
+ args: { version: { type: 'boolean', alias: 'v', description: 'Show the RunX version.' }, ...helpArgs },
147
+ subCommands: { list, describe, run, check, init, agent, upgrade, uninstall },
197
148
  setup: async (context) => {
198
- state.globalArgs = context.args;
199
- state.usageCommand = resolveUsageCommand(context.args._, usageCommands, state);
200
- await validateArguments(context, state);
201
- if (state.globalArgs.version || state.globalArgs.v)
202
- return handleOutput(`${readVersion()}\n`);
203
- if (state.globalArgs.helpTree)
204
- return handleOutput(`${showHelpTree()}\n`);
205
- if (state.globalArgs.helpDocs)
206
- return handleOutput(showHelpDocs());
207
- if (state.globalArgs.help || state.globalArgs.h)
208
- return handleUsage(state.usageCommand);
149
+ state.args = context.args;
150
+ state.command = resolveHelpCommand(state);
151
+ await validateInvocation(state);
152
+ if (state.args.v || state.args.version === true)
153
+ return handled(`${readVersion()}\n`);
154
+ await handleHelp(state);
155
+ },
156
+ run: ({ args }) => {
157
+ if (args._.length === 0)
158
+ write(`Hello Windows - runx v${readVersion()}\n`);
209
159
  },
210
160
  });
211
- state.usageCommand = command;
212
- return { command, state };
213
- };
161
+ state.commands.set('', root);
162
+ state.command = root;
163
+ return { command: root, state };
164
+ }
165
+ async function validateInvocation(state) {
166
+ const first = state.args._[0];
167
+ const topLevel = new Set(['list', 'describe', 'run', 'check', 'init', 'agent', 'upgrade', 'uninstall']);
168
+ if (first && !topLevel.has(first))
169
+ throw await usageError(state, `Unknown command: ${first}`);
170
+ const allowed = new Set(['_', 'h', 'v']);
171
+ for (const command of state.commands.values()) {
172
+ for (const key of Object.keys(command.args ?? {})) {
173
+ allowed.add(key);
174
+ allowed.add(key.replace(/-([a-z])/g, (_, character) => character.toUpperCase()));
175
+ }
176
+ }
177
+ const unknown = Object.keys(state.args).find((key) => !allowed.has(key));
178
+ if (unknown)
179
+ throw await usageError(state, `Unknown option: --${unknown.replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`)}`);
180
+ }
181
+ function group(name, description, subCommands, state, defaultCommand) {
182
+ const command = defineCommand({ meta: { name, description }, args: helpArgs, subCommands, ...(defaultCommand ? { default: defaultCommand } : {}), setup: helpSetup(state) });
183
+ state.commands.set(name.replace(/^runx(?:\s+|$)/, ''), command);
184
+ return command;
185
+ }
186
+ function helpSetup(state) {
187
+ return async (context) => {
188
+ state.args = context.args;
189
+ state.command = context.cmd;
190
+ await handleHelp(state);
191
+ };
192
+ }
193
+ async function handleHelp(state) {
194
+ if (state.args.helpDocs)
195
+ return handled(renderHelpDocs(state.command));
196
+ if (state.args.helpTree || state.args.helpTreeDepth) {
197
+ const depth = state.args.helpTreeDepth ? positiveInteger(state.args.helpTreeDepth, '--help-tree-depth') : Number.POSITIVE_INFINITY;
198
+ return handled(renderHelpTree(state.command, depth));
199
+ }
200
+ if (state.args.help || state.args.h)
201
+ return handled(`${await renderUsage(state.command)}\n`);
202
+ }
203
+ function resolveHelpCommand(state) {
204
+ const tokens = state.args._.filter((token) => !token.startsWith('-'));
205
+ for (let length = tokens.length; length > 0; length -= 1) {
206
+ const command = state.commands.get(tokens.slice(0, length).join(' '));
207
+ if (command)
208
+ return command;
209
+ }
210
+ return state.commands.get('') ?? state.command;
211
+ }
214
212
  const { command: runxCommand } = createCommandTree();
215
213
  async function runCli(rawArgs = process.argv.slice(2)) {
214
+ if (rawArgs.length === 1 && rawArgs[0] === '--check-updates-worker') {
215
+ await runUpdateWorker();
216
+ return;
217
+ }
216
218
  const { command, state } = createCommandTree();
219
+ const cleanOutput = rawArgs.some((arg) => ['-h', '--help', '-v', '--version', '--help-tree', '--help-docs'].includes(arg) || arg.startsWith('--help-tree-depth'));
220
+ if (!cleanOutput && rawArgs.length > 0) {
221
+ const notice = await readCachedUpdateNotice(rawArgs.includes('--verbose'));
222
+ if (notice)
223
+ process.stderr.write(`${notice}\n`);
224
+ }
225
+ spawnUpdateWorker();
217
226
  try {
218
227
  await runCittyCommand(command, { rawArgs });
219
228
  }
@@ -222,9 +231,8 @@ async function runCli(rawArgs = process.argv.slice(2)) {
222
231
  return;
223
232
  if (error instanceof CliUsageError)
224
233
  throw error;
225
- if (isCittyUsageError(error)) {
226
- throw new CliUsageError(error.message, await renderUsage(state.usageCommand));
227
- }
234
+ if (error instanceof Error && error.name === 'CLIError')
235
+ throw new CliUsageError(error.message, await renderUsage(state.command));
228
236
  throw error;
229
237
  }
230
238
  }
@@ -235,165 +243,137 @@ async function runCliWithErrorHandling(rawArgs) {
235
243
  catch (error) {
236
244
  if (error instanceof CliUsageError) {
237
245
  process.stderr.write(`${error.usage}\n\nerror: ${error.message}\n`);
238
- process.exitCode = 1;
239
- return;
246
+ process.exitCode = 2;
240
247
  }
241
- if (error instanceof RunXError) {
248
+ else if (error instanceof RunXError) {
242
249
  process.stderr.write(`error: ${error.message}\n`);
243
250
  process.exitCode = error.exitCode;
244
- return;
245
251
  }
246
- process.stderr.write(`error: ${error instanceof Error ? error.message : 'Unexpected failure.'}\n`);
247
- process.exitCode = 1;
248
- }
249
- }
250
- function createRunCommand(state, selectorDefault) {
251
- return defineCommand({
252
- meta: {
253
- name: selectorDefault ? 'runx <selector>' : 'runx run',
254
- alias: selectorDefault ? undefined : 'r',
255
- description: selectorDefault ? 'Run a selector using the root shorthand.' : 'Run one manifest command.',
256
- },
257
- args: selectorDefault
258
- ? runArgs
259
- : {
260
- selector: { type: 'positional', description: 'Command UID, group/id, index, or unambiguous ID.' },
261
- ...runArgs,
262
- },
263
- setup: withCommandHelp(state),
264
- run: async ({ args }) => {
265
- const selector = selectorDefault ?? (typeof args.selector === 'string' ? args.selector : undefined);
266
- if (!selector)
267
- throw await missingSelector(state);
268
- await runSelectedCommand(selector, resolveOptions(state.globalArgs), state.globalArgs);
269
- },
270
- });
271
- }
272
- function createSelectorCompatibleCommands(commands, state) {
273
- const aliases = new Set(['r']);
274
- return new Proxy(commands, {
275
- has: (target, property) => {
276
- if (typeof property !== 'string')
277
- return Reflect.has(target, property);
278
- if (aliases.has(property))
279
- return false;
280
- return true;
281
- },
282
- get: (target, property, receiver) => {
283
- if (typeof property !== 'string' || Reflect.has(target, property))
284
- return Reflect.get(target, property, receiver);
285
- return createRunCommand(state, property);
286
- },
287
- });
288
- }
289
- function resolveUsageCommand(positionals, commands, state) {
290
- const name = positionals[0];
291
- if (!name)
292
- return state.usageCommand;
293
- if (!commands.has(name))
294
- return createRunCommand(state, name);
295
- for (let length = positionals.length; length > 0; length -= 1) {
296
- const command = commands.get(positionals.slice(0, length).join(' '));
297
- if (command)
298
- return command;
252
+ else {
253
+ process.stderr.write(`error: ${error instanceof Error ? error.message : 'Unexpected failure.'}\n`);
254
+ process.exitCode = 1;
255
+ }
299
256
  }
300
- return commands.get(name);
301
- }
302
- function withCommandHelp(state) {
303
- return (context) => {
304
- state.usageCommand = context.cmd;
305
- };
306
- }
307
- async function missingSelector(state) {
308
- return new CliUsageError('Missing required positional argument: SELECTOR', await renderUsage(state.usageCommand));
309
- }
310
- async function validateArguments(context, state) {
311
- const unknown = Object.keys(context.args).find((key) => !knownArgumentKeys.has(key));
312
- if (!unknown)
313
- return;
314
- throw new CliUsageError(`Unknown option --${unknown}`, await renderUsage(state.usageCommand));
315
- }
316
- async function handleUsage(command) {
317
- return handleOutput(`${await renderUsage(command)}\n`);
318
257
  }
319
- function handleOutput(value) {
320
- write(value);
321
- throw new CliHandled();
258
+ async function load(optionsValue) {
259
+ const loaded = await readManifest(optionsValue.cwd, optionsValue.config);
260
+ const message = `configuration file loaded: ${loaded.path}\n`;
261
+ if (optionsValue.format === 'json')
262
+ process.stderr.write(message);
263
+ else
264
+ write(message);
265
+ return loaded;
322
266
  }
323
- async function listCommands(options) {
324
- const { manifest, path } = await readManifest(options.cwd, options.file);
325
- write(options.format === 'json' ? renderJson({ manifestPath: path, manifest }) : renderList(manifest, path));
267
+ async function listCommands(optionsValue) {
268
+ const { manifest, path } = await load(optionsValue);
269
+ write(optionsValue.format === 'json' ? renderJson({ manifestPath: path, manifest }) : renderList(manifest, path));
326
270
  }
327
- async function describeCommand(selector, options) {
328
- const { manifest, path } = await readManifest(options.cwd, options.file);
271
+ async function describeCommand(selector, optionsValue) {
272
+ const { manifest, path } = await load(optionsValue);
329
273
  const command = resolveCommand(manifest, path, selector);
330
- write(options.format === 'json' ? renderJson(command) : renderDescription(command));
274
+ write(optionsValue.format === 'json' ? renderJson(command) : renderDescription(command));
331
275
  }
332
- async function checkManifest(options) {
333
- const { manifest, path } = await readManifest(options.cwd, options.file);
276
+ async function checkManifest(optionsValue) {
277
+ const { manifest, path } = await load(optionsValue);
334
278
  const result = { valid: true, manifestPath: path, commandCount: manifest.commands.length, groups: Object.keys(manifest.groups) };
335
- write(options.format === 'json' ? renderJson(result) : `valid: true\nmanifest: ${path}\ncommands: ${result.commandCount}\n`);
279
+ write(optionsValue.format === 'json' ? renderJson(result) : `valid: true\nmanifest: ${path}\ncommands: ${result.commandCount}\n`);
336
280
  }
337
- async function runSelectedCommand(selector, options, args) {
338
- const { manifest, path } = await readManifest(options.cwd, options.file);
281
+ async function runSelected(selector, optionsValue, args) {
282
+ const { manifest, path } = await load(optionsValue);
339
283
  const command = resolveCommand(manifest, path, selector);
340
284
  if (args.dryRun) {
341
- write(options.format === 'json' ? renderJson({ dryRun: true, command }) : renderExecutionPlan(command));
285
+ write(optionsValue.format === 'json' ? renderJson({ dryRun: true, command }) : renderExecutionPlan(command));
342
286
  return;
343
287
  }
344
- if (command.confirm === 'always' && !args.yes) {
345
- throw new RunXError(`Command ${command.uid} requires confirmation. Review it with \`runx describe ${command.uid}\` and rerun with --yes when authorized.`);
346
- }
347
- if (options.format === 'text')
288
+ if (command.confirm === 'always' && !args.yes)
289
+ throw new RunXError(`Command ${command.uid} requires confirmation. Rerun with --yes only after authorization.`, 2);
290
+ if (optionsValue.format === 'text')
348
291
  write(`Running ${command.uid} (${command.selector})\n`);
349
292
  process.exitCode = await runCommand(command);
350
293
  }
351
- async function runAgentsInstall(scopeValue, options, args) {
352
- const scope = scopeValue;
353
- if (scope !== 'local' && scope !== 'global')
354
- throw new RunXError('Agent scope must be local or global.');
355
- const tool = (args.tool ?? 'agents');
356
- if (!['agents', 'claude', 'all'].includes(tool))
357
- throw new RunXError('Agent tool must be agents, claude, or all.');
358
- const installed = await installAgentSkill(scope, tool, options.cwd);
359
- write(options.format === 'json' ? renderJson({ installed }) : `${installed.map((path) => `installed: ${path}`).join('\n')}\n`);
294
+ async function agentMutation(label, action, args) {
295
+ try {
296
+ const paths = await action(args.local ? 'local' : 'global', resolvePath(args.cwd ?? process.cwd()));
297
+ writeFormatted({ [label]: paths }, args);
298
+ }
299
+ catch (error) {
300
+ if (error instanceof RunXError)
301
+ throw error;
302
+ throw new RunXError(`Agent skill mutation failed: ${error instanceof Error ? error.message : String(error)}`, 5);
303
+ }
304
+ }
305
+ async function instructionMutation(label, action, args) {
306
+ try {
307
+ const paths = await action(resolvePath(args.cwd ?? process.cwd()));
308
+ writeFormatted({ [label]: paths }, args);
309
+ }
310
+ catch (error) {
311
+ if (error instanceof RunXError)
312
+ throw error;
313
+ throw new RunXError(`Agent instruction mutation failed: ${error instanceof Error ? error.message : String(error)}`, 5);
314
+ }
315
+ }
316
+ async function runUpgradeList(args) {
317
+ const requestedArch = args.arch ? decode(archSchema, args.arch, '--arch') : process.arch;
318
+ const platform = resolveUpgradePlatform(process.platform, requestedArch);
319
+ const variant = decode(variantSchema, args.variant ?? 'baseline', '--variant');
320
+ const page = positiveInteger(args.page ?? '1', '--page');
321
+ const perPage = positiveInteger(args.perPage ?? '20', '--per-page');
322
+ const catalog = await fetchReleaseCatalog({ ...platform, variant, currentVersion: readVersion() });
323
+ const filtered = args.preReleases ? catalog.releases : catalog.releases.filter((release) => !release.prerelease);
324
+ const paged = { ...catalog, releases: filtered.slice((page - 1) * perPage, page * perPage) };
325
+ write(options(args).format === 'json' ? renderJson(paged) : renderReleaseCatalog(paged));
360
326
  }
361
- async function runAgentsInstructions(options) {
362
- const path = await installAgentInstructions(options.cwd);
363
- write(options.format === 'json' ? renderJson({ instructions: path }) : `updated: ${path}\n`);
327
+ async function runUpgrade(optionsValue, args) {
328
+ if (args.arch)
329
+ decode(archSchema, args.arch, '--arch');
330
+ if (args.variant)
331
+ decode(variantSchema, args.variant, '--variant');
332
+ const text = optionsValue.format === 'text';
333
+ if (text)
334
+ write(renderUpgradeHeading());
335
+ const result = await upgradeSelf({
336
+ dryRun: Boolean(args.dryRun),
337
+ requestedVersion: typeof args.version === 'string' ? args.version : undefined,
338
+ arch: args.arch,
339
+ variant: args.variant,
340
+ onPlan: text ? (plan) => write(renderUpgradePlan(plan)) : undefined,
341
+ onEvent: text ? (event) => write(renderUpgradeEvent(event)) : undefined,
342
+ });
343
+ write(text ? renderUpgradeResult(result) : renderJson(result));
344
+ if (result.outcome === 'failed' || result.outcome === 'rolled-back')
345
+ process.exitCode = result.error?.code.includes('download') ? 4 : 5;
364
346
  }
365
- async function runUpgradeCheck(options) {
366
- const result = await checkForLatestVersion();
367
- write(options.format === 'json' ? renderJson(result) : `current: ${result.currentVersion}\nlatest: ${result.latestVersion}\nupdate_available: ${result.updateAvailable}\n`);
347
+ function options(args) {
348
+ const format = decode(formatSchema, args.format ?? 'text', '--format');
349
+ return { cwd: resolvePath(args.cwd ?? process.cwd()), config: args.config, format: format, verbose: Boolean(args.verbose) };
368
350
  }
369
- async function runUpgradeList(options) {
370
- const versions = await listAvailableVersions();
371
- write(options.format === 'json' ? renderJson({ versions }) : `${versions.join('\n')}\n`);
351
+ function decode(schema, value, flag) {
352
+ try {
353
+ return Value.Decode(schema, value);
354
+ }
355
+ catch {
356
+ throw new RunXError(`Invalid ${flag} value: ${String(value)}`, 2);
357
+ }
372
358
  }
373
- async function runUpgrade(options, args) {
374
- const result = await upgradeSelf(Boolean(args.dryRun));
375
- if (options.format === 'json')
376
- return write(renderJson(result));
377
- const summary = `current: ${result.currentVersion}\ntarget: ${result.latestVersion}\n`;
378
- write(result.upToDate ? `${summary}Already up to date.\n` : `${summary}path: ${result.executablePath}\n${result.scheduled ? 'scheduled: true\n' : ''}`);
359
+ function positiveInteger(value, flag) {
360
+ const number = Number(value);
361
+ try {
362
+ return Value.Decode(positiveIntegerSchema, number);
363
+ }
364
+ catch {
365
+ throw new RunXError(`${flag} must be a positive integer.`, 2);
366
+ }
379
367
  }
380
- async function runUninstall(options, args) {
381
- const result = await uninstallSelf(Boolean(args.dryRun));
382
- write(options.format === 'json' ? renderJson(result) : `path: ${result.executablePath}\n${result.dryRun ? 'dry_run: true\n' : result.scheduled ? 'scheduled: true\n' : 'uninstalled: true\n'}`);
368
+ async function usageError(state, message) {
369
+ return new CliUsageError(message, await renderUsage(state.command));
383
370
  }
384
- function resolveOptions(args) {
385
- const format = args.format ?? 'text';
386
- if (format !== 'text' && format !== 'json')
387
- throw new RunXError('Invalid --format value. Expected text or json.');
388
- return {
389
- cwd: resolve(args.cwd ?? process.cwd()),
390
- file: args.file,
391
- format: format,
392
- verbose: Boolean(args.verbose),
393
- };
371
+ function writeFormatted(value, args) {
372
+ write(options(args).format === 'json' ? renderJson(value) : `${typeof value === 'string' ? value : JSON.stringify(value, null, 2)}\n`);
394
373
  }
395
- function isCittyUsageError(error) {
396
- return error instanceof Error && error.name === 'CLIError';
374
+ function handled(value) {
375
+ write(value.endsWith('\n') ? value : `${value}\n`);
376
+ throw new CliHandled();
397
377
  }
398
378
  function write(value) {
399
379
  process.stdout.write(value);