@guiho/runx 0.2.5 → 0.2.7
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/CHANGELOG.md +31 -0
- package/DOCS.md +31 -9
- package/README.md +22 -3
- package/docs/decisions/decisions.xdocs.md +7 -0
- package/docs/decisions/interactive-init-manifest.md +187 -0
- package/docs/decisions/windows-self-upgrade.md +98 -0
- package/docs/plans/interactive-init-manifest.md +177 -0
- package/docs/plans/plans.xdocs.md +7 -0
- package/docs/plans/windows-self-upgrade.md +129 -0
- package/docs/reviews/implementation/implementation.xdocs.md +7 -0
- package/docs/reviews/implementation/interactive-init-manifest-review.md +69 -0
- package/docs/reviews/implementation/windows-self-upgrade-review.md +79 -0
- package/docs/reviews/plans/interactive-init-manifest-review.md +82 -0
- package/docs/reviews/plans/plans.xdocs.md +7 -0
- package/docs/reviews/plans/windows-self-upgrade-review.md +71 -0
- package/docs/validation/interactive-init-manifest.md +79 -0
- package/docs/validation/validation.xdocs.md +7 -0
- package/docs/validation/windows-self-upgrade.md +85 -0
- package/library/cli.d.ts.map +1 -1
- package/library/cli.js +18 -0
- package/library/help.d.ts.map +1 -1
- package/library/help.js +4 -0
- package/library/init.d.ts +37 -0
- package/library/init.d.ts.map +1 -0
- package/library/init.js +165 -0
- package/library/manifest.d.ts +4 -1
- package/library/manifest.d.ts.map +1 -1
- package/library/manifest.js +15 -2
- package/library/self-management.d.ts +5 -4
- package/library/self-management.d.ts.map +1 -1
- package/library/self-management.js +66 -9
- package/package.json +1 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: RunX Windows Self-Upgrade Validation
|
|
3
|
+
purpose: Record verification evidence for the Windows self-upgrade fix and issue closure.
|
|
4
|
+
description: Captures Windows replacement and rollback tests, complete Bun build checks, native CLI checks, CI syntax, XDocs health, and residual release risk.
|
|
5
|
+
created: 2026-07-14
|
|
6
|
+
flags:
|
|
7
|
+
- validated
|
|
8
|
+
tags:
|
|
9
|
+
- validation
|
|
10
|
+
- windows
|
|
11
|
+
- cli
|
|
12
|
+
keywords:
|
|
13
|
+
- runx upgrade
|
|
14
|
+
- windows executable
|
|
15
|
+
- rollback
|
|
16
|
+
- issue 9
|
|
17
|
+
- issue 1
|
|
18
|
+
owner: runx-validation
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# RunX Windows Self-Upgrade Validation
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
The implementation is ready for commit, issue closure, and the requested
|
|
26
|
+
Mirror-managed patch. All local checks passed on Windows.
|
|
27
|
+
|
|
28
|
+
## Scope
|
|
29
|
+
|
|
30
|
+
- Synchronous Windows replacement of a running native executable.
|
|
31
|
+
- Target-version verification before successful completion.
|
|
32
|
+
- Rollback after invalid replacement verification.
|
|
33
|
+
- Deferred cleanup of the locked old executable only.
|
|
34
|
+
- Existing manifest-free `-h` behavior from issue #1.
|
|
35
|
+
- Windows CI coverage and structured documentation health.
|
|
36
|
+
|
|
37
|
+
## Commands and Results
|
|
38
|
+
|
|
39
|
+
| Check | Result | Evidence |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `bun run typecheck` | passed | TypeScript completed without diagnostics. |
|
|
42
|
+
| `bun test` | passed | 15 tests, 0 failures, 134 assertions. |
|
|
43
|
+
| `bun run build` | passed | Library output compiled. |
|
|
44
|
+
| `bun run binary` | passed | Local Windows executable compiled. |
|
|
45
|
+
| `bun run binaries` | passed | All twelve release assets built and verified. |
|
|
46
|
+
| Native `runx-windows-x64.exe -h` outside a manifest | passed | Citty usage rendered with exit code 0. |
|
|
47
|
+
| Native `runx-windows-x64.exe --version` | passed | Reported `0.2.5` before the patch bump. |
|
|
48
|
+
| `Bun.YAML.parse(.github/workflows/ci.yml)` | passed | Workflow YAML parsed successfully. |
|
|
49
|
+
| `xdocs meta . --documents --strict --owner runx --format json` | passed | Root metadata and companions valid. |
|
|
50
|
+
| `xdocs doctor . --warnings-as-errors --format json` | passed | 0 errors and 0 warnings. |
|
|
51
|
+
| `git diff --check` | passed | No whitespace errors. |
|
|
52
|
+
|
|
53
|
+
## Behavioral Evidence
|
|
54
|
+
|
|
55
|
+
`source/self-management.spec.ts` runs only on Windows for the platform-specific
|
|
56
|
+
cases. Its success case launches a copied PowerShell executable from the
|
|
57
|
+
configured self path, keeps that executable running, upgrades the same path to
|
|
58
|
+
the current Bun executable, and requires the replacement to report the expected
|
|
59
|
+
version before `upgradeSelf()` resolves. It also observes removal of `.new` and
|
|
60
|
+
eventual removal of `.old` after the running process exits.
|
|
61
|
+
|
|
62
|
+
The rollback case downloads invalid executable bytes, requires verification to
|
|
63
|
+
fail, and confirms that the original bytes are restored with neither `.new` nor
|
|
64
|
+
`.old` left behind.
|
|
65
|
+
|
|
66
|
+
The existing `source/cli.spec.ts` root-help test proves both `-h` and `--help`
|
|
67
|
+
work without a manifest, satisfying issue #1.
|
|
68
|
+
|
|
69
|
+
## Skipped or Blocked Checks
|
|
70
|
+
|
|
71
|
+
- The new GitHub-hosted `windows-latest` job is not locally runnable; it will
|
|
72
|
+
execute after the commit reaches GitHub.
|
|
73
|
+
- An actual 0.2.0-to-new-patch public upgrade is blocked until the new release
|
|
74
|
+
assets are published.
|
|
75
|
+
|
|
76
|
+
## Readiness
|
|
77
|
+
|
|
78
|
+
Ready for commit, closing GitHub issues #9 and #1, changelog preparation, and
|
|
79
|
+
the requested Mirror patch plan/apply sequence.
|
|
80
|
+
|
|
81
|
+
## References
|
|
82
|
+
|
|
83
|
+
- [Implementation Review](../reviews/implementation/windows-self-upgrade-review.md)
|
|
84
|
+
- [Plan](../plans/windows-self-upgrade.md)
|
|
85
|
+
- [Decision](../decisions/windows-self-upgrade.md)
|
package/library/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../source/cli.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../source/cli.ts"],"names":[],"mappings":"AAAA;;GAEG;AAaH,OAAO,KAAK,EAAkB,UAAU,EAAkB,MAAM,OAAO,CAAA;AAGvE,OAAO,EACL,MAAM,EACN,uBAAuB,EACvB,WAAW,GACZ,CAAA;AA8PD,QAAA,MAAiB,WAAW,iBAAwB,CAAA;AAEpD,iBAAe,MAAM,CAAC,OAAO,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAY9E;AAED,iBAAe,uBAAuB,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBxE"}
|
package/library/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { installAgentInstructions, installAgentSkill } from './agents.js';
|
|
|
7
7
|
import { RunXError } from './errors.js';
|
|
8
8
|
import { runCommand } from './executor.js';
|
|
9
9
|
import { readVersion, showHelpDocs, showHelpTree, showHome } from './help.js';
|
|
10
|
+
import { initializeRunXManifest } from './init.js';
|
|
10
11
|
import { readManifest, resolveCommand } from './manifest.js';
|
|
11
12
|
import { renderDescription, renderExecutionPlan, renderJson, renderList } from './render.js';
|
|
12
13
|
import { checkForLatestVersion, listAvailableVersions, uninstallSelf, upgradeSelf } from './self-management.js';
|
|
@@ -95,6 +96,12 @@ const createCommandTree = () => {
|
|
|
95
96
|
setup: withCommandHelp(state),
|
|
96
97
|
run: async () => checkManifest(resolveOptions(state.globalArgs)),
|
|
97
98
|
});
|
|
99
|
+
const initCommand = defineCommand({
|
|
100
|
+
meta: { name: 'runx init', description: 'Interactively create an empty RunX manifest.' },
|
|
101
|
+
args: commonArgs,
|
|
102
|
+
setup: withCommandHelp(state),
|
|
103
|
+
run: async () => initializeProject(state.globalArgs),
|
|
104
|
+
});
|
|
98
105
|
const agentsInstallCommand = defineCommand({
|
|
99
106
|
meta: { name: 'runx agents install', description: 'Install the bundled RunX skill.' },
|
|
100
107
|
args: {
|
|
@@ -166,6 +173,7 @@ const createCommandTree = () => {
|
|
|
166
173
|
describe: describeCommandDefinition,
|
|
167
174
|
run: runCommandDefinition,
|
|
168
175
|
check: checkCommand,
|
|
176
|
+
init: initCommand,
|
|
169
177
|
agents: agentsCommand,
|
|
170
178
|
upgrade: upgradeCommand,
|
|
171
179
|
uninstall: uninstallCommand,
|
|
@@ -176,6 +184,7 @@ const createCommandTree = () => {
|
|
|
176
184
|
['run', runCommandDefinition],
|
|
177
185
|
['r', runCommandDefinition],
|
|
178
186
|
['check', checkCommand],
|
|
187
|
+
['init', initCommand],
|
|
179
188
|
['agents', agentsCommand],
|
|
180
189
|
['agents install', agentsInstallCommand],
|
|
181
190
|
['agents instructions', agentsInstructionsCommand],
|
|
@@ -334,6 +343,15 @@ async function checkManifest(options) {
|
|
|
334
343
|
const result = { valid: true, manifestPath: path, commandCount: manifest.commands.length, groups: Object.keys(manifest.groups) };
|
|
335
344
|
write(options.format === 'json' ? renderJson(result) : `valid: true\nmanifest: ${path}\ncommands: ${result.commandCount}\n`);
|
|
336
345
|
}
|
|
346
|
+
async function initializeProject(args) {
|
|
347
|
+
if (args.file) {
|
|
348
|
+
throw new RunXError('runx init does not support --file. It always creates runx.yaml in --cwd or the current directory.');
|
|
349
|
+
}
|
|
350
|
+
if (args.format === 'json') {
|
|
351
|
+
throw new RunXError('runx init does not support --format json because initialization is an interactive terminal workflow.');
|
|
352
|
+
}
|
|
353
|
+
await initializeRunXManifest({ cwd: resolve(args.cwd ?? process.cwd()) });
|
|
354
|
+
}
|
|
337
355
|
async function runSelectedCommand(selector, options, args) {
|
|
338
356
|
const { manifest, path } = await readManifest(options.cwd, options.file);
|
|
339
357
|
const command = resolveCommand(manifest, path, selector);
|
package/library/help.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../source/help.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,QAAO,MAAiF,CAAA;AAEhH,eAAO,MAAM,QAAQ,QAAO,
|
|
1
|
+
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../source/help.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,QAAO,MAAiF,CAAA;AAEhH,eAAO,MAAM,QAAQ,QAAO,MAiB3B,CAAA;AAED,eAAO,MAAM,YAAY,QAAO,MAcpB,CAAA;AAEZ,eAAO,MAAM,YAAY,QAAO,MAW/B,CAAA"}
|
package/library/help.js
CHANGED
|
@@ -5,6 +5,7 @@ export const showHome = () => `RunX ${readVersion()}
|
|
|
5
5
|
A documented, local command catalog for runx.yaml manifests.
|
|
6
6
|
|
|
7
7
|
Usage:
|
|
8
|
+
runx init [--cwd <path>]
|
|
8
9
|
runx list [--file <path>] [--format <text|json>]
|
|
9
10
|
runx describe <selector>
|
|
10
11
|
runx run <selector> [--dry-run] [--yes]
|
|
@@ -12,6 +13,7 @@ Usage:
|
|
|
12
13
|
runx <selector>
|
|
13
14
|
|
|
14
15
|
Start here:
|
|
16
|
+
runx init Interactively create an empty runx.yaml catalog.
|
|
15
17
|
runx list List every command in the nearest manifest.
|
|
16
18
|
runx --help-tree Show the complete command tree.
|
|
17
19
|
runx --help-docs Show manifest and agent documentation guidance.
|
|
@@ -23,6 +25,7 @@ export const showHelpTree = () => [
|
|
|
23
25
|
'|- run <selector>',
|
|
24
26
|
'| `- alias: r',
|
|
25
27
|
'|- check',
|
|
28
|
+
'|- init',
|
|
26
29
|
'|- agents',
|
|
27
30
|
'| |- install <local|global>',
|
|
28
31
|
'| `- instructions',
|
|
@@ -33,6 +36,7 @@ export const showHelpTree = () => [
|
|
|
33
36
|
export const showHelpDocs = () => `RunX documentation
|
|
34
37
|
|
|
35
38
|
Manifest: runx.yaml, discovered from the current directory upward or selected with --file.
|
|
39
|
+
Create an empty catalog with runx init. Its manifest uses SemVer 1.x, configures a scripts directory, and always includes the public group.
|
|
36
40
|
Required command fields: uid, id, group, summary, description, command.
|
|
37
41
|
Optional command fields: cwd, shell, tags, confirm.
|
|
38
42
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
|
|
3
|
+
*/
|
|
4
|
+
import type { RunXManifest } from './types.js';
|
|
5
|
+
export type RunXInitPrompter = {
|
|
6
|
+
intro(title: string): void;
|
|
7
|
+
text(options: {
|
|
8
|
+
message: string;
|
|
9
|
+
initialValue: string;
|
|
10
|
+
validate: (value: string | undefined) => string | undefined;
|
|
11
|
+
}): Promise<string | undefined>;
|
|
12
|
+
confirm(options: {
|
|
13
|
+
message: string;
|
|
14
|
+
initialValue: boolean;
|
|
15
|
+
}): Promise<boolean | undefined>;
|
|
16
|
+
preview(manifest: string): void;
|
|
17
|
+
cancel(message: string): void;
|
|
18
|
+
outro(message: string): void;
|
|
19
|
+
close?(): void;
|
|
20
|
+
};
|
|
21
|
+
export type RunXInitResult = {
|
|
22
|
+
status: 'cancelled';
|
|
23
|
+
} | {
|
|
24
|
+
status: 'created';
|
|
25
|
+
path: string;
|
|
26
|
+
manifest: RunXManifest;
|
|
27
|
+
};
|
|
28
|
+
type RunXInitOptions = {
|
|
29
|
+
cwd: string;
|
|
30
|
+
isInteractive?: boolean;
|
|
31
|
+
prompter?: RunXInitPrompter;
|
|
32
|
+
};
|
|
33
|
+
export declare const initializeRunXManifest: ({ cwd, isInteractive, prompter }: RunXInitOptions) => Promise<RunXInitResult>;
|
|
34
|
+
export declare const createInitialManifest: (projectName: string, scriptsDirectory: string) => RunXManifest;
|
|
35
|
+
export declare const renderInitialManifest: (manifest: RunXManifest) => string;
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../source/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,IAAI,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,GAAG,SAAS,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAClJ,OAAO,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAA;IAC1F,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,KAAK,CAAC,IAAI,IAAI,CAAA;CACf,CAAA;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,YAAY,CAAA;CAAE,CAAA;AAE/D,KAAK,eAAe,GAAG;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,EAAE,gBAAgB,CAAA;CAC5B,CAAA;AAID,eAAO,MAAM,sBAAsB,qCAA4G,eAAe,KAAG,OAAO,CAAC,cAAc,CAmDtL,CAAA;AAED,eAAO,MAAM,qBAAqB,gBAAiB,MAAM,oBAAoB,MAAM,KAAG,YAQpF,CAAA;AAEF,eAAO,MAAM,qBAAqB,aAAc,YAAY,KAAG,MAenD,CAAA"}
|
package/library/init.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { rename, rm, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { basename, isAbsolute, relative, resolve } from 'node:path';
|
|
7
|
+
import { createInterface } from 'node:readline/promises';
|
|
8
|
+
import { RunXError } from './errors.js';
|
|
9
|
+
import { readManifest } from './manifest.js';
|
|
10
|
+
const manifestVersion = '1.0.0';
|
|
11
|
+
export const initializeRunXManifest = async ({ cwd, isInteractive = process.stdin.isTTY === true && process.stdout.isTTY === true, prompter }) => {
|
|
12
|
+
if (!isInteractive) {
|
|
13
|
+
throw new RunXError('runx init requires an interactive terminal. Run it directly in a terminal with stdin and stdout attached.');
|
|
14
|
+
}
|
|
15
|
+
const activePrompter = prompter ?? createTerminalPrompter();
|
|
16
|
+
try {
|
|
17
|
+
const root = resolve(cwd);
|
|
18
|
+
const path = resolve(root, 'runx.yaml');
|
|
19
|
+
const defaultProjectName = basename(root) || 'my-project';
|
|
20
|
+
activePrompter.intro('RunX project setup');
|
|
21
|
+
if (await Bun.file(path).exists()) {
|
|
22
|
+
const overwrite = await activePrompter.confirm({
|
|
23
|
+
message: 'runx.yaml already exists. Replace it?',
|
|
24
|
+
initialValue: false,
|
|
25
|
+
});
|
|
26
|
+
if (overwrite !== true)
|
|
27
|
+
return cancelInitialization(activePrompter);
|
|
28
|
+
}
|
|
29
|
+
const projectName = await activePrompter.text({
|
|
30
|
+
message: 'Project name',
|
|
31
|
+
initialValue: defaultProjectName,
|
|
32
|
+
validate: (value) => value?.trim() ? undefined : 'Enter a project name.',
|
|
33
|
+
});
|
|
34
|
+
if (projectName === undefined)
|
|
35
|
+
return cancelInitialization(activePrompter);
|
|
36
|
+
const scriptsDirectory = await activePrompter.text({
|
|
37
|
+
message: 'Scripts directory',
|
|
38
|
+
initialValue: 'scripts',
|
|
39
|
+
validate: (value) => validateScriptsDirectory(value, root),
|
|
40
|
+
});
|
|
41
|
+
if (scriptsDirectory === undefined)
|
|
42
|
+
return cancelInitialization(activePrompter);
|
|
43
|
+
const manifest = createInitialManifest(projectName.trim(), scriptsDirectory.trim());
|
|
44
|
+
const yaml = renderInitialManifest(manifest);
|
|
45
|
+
activePrompter.preview(yaml);
|
|
46
|
+
const confirmed = await activePrompter.confirm({
|
|
47
|
+
message: 'Create this runx.yaml?',
|
|
48
|
+
initialValue: true,
|
|
49
|
+
});
|
|
50
|
+
if (confirmed !== true)
|
|
51
|
+
return cancelInitialization(activePrompter);
|
|
52
|
+
await writeValidatedManifest(root, path, yaml);
|
|
53
|
+
activePrompter.outro(`Created ${path}`);
|
|
54
|
+
return { status: 'created', path, manifest };
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
activePrompter.close?.();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
export const createInitialManifest = (projectName, scriptsDirectory) => ({
|
|
61
|
+
version: manifestVersion,
|
|
62
|
+
project: { name: projectName },
|
|
63
|
+
scripts: { directory: scriptsDirectory },
|
|
64
|
+
groups: {
|
|
65
|
+
public: { summary: 'Default public project commands.' },
|
|
66
|
+
},
|
|
67
|
+
commands: [],
|
|
68
|
+
});
|
|
69
|
+
export const renderInitialManifest = (manifest) => [
|
|
70
|
+
`version: ${JSON.stringify(manifest.version)}`,
|
|
71
|
+
'',
|
|
72
|
+
'project:',
|
|
73
|
+
` name: ${JSON.stringify(manifest.project?.name ?? '')}`,
|
|
74
|
+
'',
|
|
75
|
+
'scripts:',
|
|
76
|
+
` directory: ${JSON.stringify(manifest.scripts.directory)}`,
|
|
77
|
+
'',
|
|
78
|
+
'groups:',
|
|
79
|
+
' public:',
|
|
80
|
+
` summary: ${JSON.stringify(manifest.groups.public.summary)}`,
|
|
81
|
+
'',
|
|
82
|
+
'commands: []',
|
|
83
|
+
'',
|
|
84
|
+
].join('\n');
|
|
85
|
+
const createTerminalPrompter = () => {
|
|
86
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
87
|
+
const accent = (value) => process.env['NO_COLOR'] ? value : `\u001b[36m${value}\u001b[0m`;
|
|
88
|
+
const success = (value) => process.env['NO_COLOR'] ? value : `\u001b[32m${value}\u001b[0m`;
|
|
89
|
+
const muted = (value) => process.env['NO_COLOR'] ? value : `\u001b[2m${value}\u001b[0m`;
|
|
90
|
+
const error = (value) => process.env['NO_COLOR'] ? value : `\u001b[31m${value}\u001b[0m`;
|
|
91
|
+
const write = (value) => {
|
|
92
|
+
process.stdout.write(value);
|
|
93
|
+
};
|
|
94
|
+
const ask = async (label) => {
|
|
95
|
+
try {
|
|
96
|
+
return await terminal.question(label);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
intro: (title) => write(`\n${accent('◆')} ${title}\n${muted(' A documented command catalog for this project.')}\n\n`),
|
|
104
|
+
text: async (options) => {
|
|
105
|
+
while (true) {
|
|
106
|
+
const value = await ask(`${accent('◆')} ${options.message} ${muted(`[${options.initialValue}]`)}\n `);
|
|
107
|
+
if (value === undefined)
|
|
108
|
+
return undefined;
|
|
109
|
+
const selected = value.trim() || options.initialValue;
|
|
110
|
+
const validation = options.validate(selected);
|
|
111
|
+
if (!validation)
|
|
112
|
+
return selected;
|
|
113
|
+
write(`${error('▲')} ${validation}\n`);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
confirm: async (options) => {
|
|
117
|
+
while (true) {
|
|
118
|
+
const defaultLabel = options.initialValue ? 'Y/n' : 'y/N';
|
|
119
|
+
const value = await ask(`${accent('◆')} ${options.message} ${muted(`[${defaultLabel}]`)}\n `);
|
|
120
|
+
if (value === undefined)
|
|
121
|
+
return undefined;
|
|
122
|
+
const answer = value.trim().toLowerCase();
|
|
123
|
+
if (!answer)
|
|
124
|
+
return options.initialValue;
|
|
125
|
+
if (answer === 'y' || answer === 'yes')
|
|
126
|
+
return true;
|
|
127
|
+
if (answer === 'n' || answer === 'no')
|
|
128
|
+
return false;
|
|
129
|
+
write(`${error('▲')} Enter yes or no.\n`);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
preview: (manifest) => write(`\n${accent('┌─ runx.yaml preview ─────────────────────────────────')}\n${manifest.trimEnd().split('\n').map((line) => `${accent('│')} ${line}`).join('\n')}\n${accent('└────────────────────────────────────────────────────')}\n\n`),
|
|
133
|
+
cancel: (message) => write(`${error('■')} ${message}\n`),
|
|
134
|
+
outro: (message) => {
|
|
135
|
+
write(`${success('◆')} ${message}\n`);
|
|
136
|
+
},
|
|
137
|
+
close: () => terminal.close(),
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const cancelInitialization = (prompter) => {
|
|
141
|
+
prompter.cancel('Initialization cancelled. No files were changed.');
|
|
142
|
+
return { status: 'cancelled' };
|
|
143
|
+
};
|
|
144
|
+
const validateScriptsDirectory = (value, root) => {
|
|
145
|
+
const directory = value?.trim();
|
|
146
|
+
if (!directory)
|
|
147
|
+
return 'Enter a scripts directory.';
|
|
148
|
+
const resolvedDirectory = resolve(root, directory);
|
|
149
|
+
const relativeDirectory = relative(root, resolvedDirectory);
|
|
150
|
+
if (relativeDirectory === '' || relativeDirectory === '.' || isAbsolute(relativeDirectory) || relativeDirectory.startsWith('..')) {
|
|
151
|
+
return 'Use a relative subdirectory inside this project.';
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
};
|
|
155
|
+
const writeValidatedManifest = async (root, path, yaml) => {
|
|
156
|
+
const temporaryPath = resolve(root, `.runx-${randomUUID()}.yaml.tmp`);
|
|
157
|
+
try {
|
|
158
|
+
await writeFile(temporaryPath, yaml, { encoding: 'utf8', flag: 'wx' });
|
|
159
|
+
await readManifest(root, temporaryPath);
|
|
160
|
+
await rename(temporaryPath, path);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await rm(temporaryPath, { force: true });
|
|
164
|
+
}
|
|
165
|
+
};
|
package/library/manifest.d.ts
CHANGED
|
@@ -12,10 +12,13 @@ export declare const CommandSchema: import("@sinclair/typebox").TObject<{
|
|
|
12
12
|
confirm: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"never">, import("@sinclair/typebox").TLiteral<"always">]>>;
|
|
13
13
|
}>;
|
|
14
14
|
export declare const ManifestSchema: import("@sinclair/typebox").TObject<{
|
|
15
|
-
version: import("@sinclair/typebox").
|
|
15
|
+
version: import("@sinclair/typebox").TString;
|
|
16
16
|
project: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
17
17
|
name: import("@sinclair/typebox").TString;
|
|
18
18
|
}>>;
|
|
19
|
+
scripts: import("@sinclair/typebox").TObject<{
|
|
20
|
+
directory: import("@sinclair/typebox").TString;
|
|
21
|
+
}>;
|
|
19
22
|
groups: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
|
|
20
23
|
summary: import("@sinclair/typebox").TString;
|
|
21
24
|
}>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../source/manifest.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAe,YAAY,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../source/manifest.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAe,YAAY,EAAE,MAAM,YAAY,CAAA;AAK5E,eAAO,MAAM,aAAa;;;;;;;;;;;EAiBS,CAAA;AAEnC,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;EAMQ,CAAA;AAInC,eAAO,MAAM,YAAY,QAAe,MAAM,iBAAiB,MAAM,KAAG,OAAO,CAAC,MAAM,CAiBrF,CAAA;AAED,eAAO,MAAM,YAAY,QAAe,MAAM,iBAAiB,MAAM,KAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAmBvH,CAAA;AAED,eAAO,MAAM,cAAc,aAAc,YAAY,gBAAgB,MAAM,YAAY,MAAM,KAAG,eAkC/F,CAAA"}
|
package/library/manifest.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Type } from '@sinclair/typebox';
|
|
|
3
3
|
import { TypeCompiler } from '@sinclair/typebox/compiler';
|
|
4
4
|
import { RunXError, invariant } from './errors.js';
|
|
5
5
|
const identifier = '^[a-z][a-z0-9-]*$';
|
|
6
|
+
const semver = '^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$';
|
|
6
7
|
export const CommandSchema = Type.Object({
|
|
7
8
|
uid: Type.String({ pattern: identifier }),
|
|
8
9
|
id: Type.String({ pattern: identifier }),
|
|
@@ -22,10 +23,11 @@ export const CommandSchema = Type.Object({
|
|
|
22
23
|
confirm: Type.Optional(Type.Union([Type.Literal('never'), Type.Literal('always')])),
|
|
23
24
|
}, { additionalProperties: false });
|
|
24
25
|
export const ManifestSchema = Type.Object({
|
|
25
|
-
version: Type.
|
|
26
|
+
version: Type.String({ pattern: semver }),
|
|
26
27
|
project: Type.Optional(Type.Object({ name: Type.String({ minLength: 1 }) }, { additionalProperties: false })),
|
|
28
|
+
scripts: Type.Object({ directory: Type.String({ minLength: 1 }) }, { additionalProperties: false }),
|
|
27
29
|
groups: Type.Record(Type.String({ pattern: identifier }), Type.Object({ summary: Type.String({ minLength: 1 }) }, { additionalProperties: false })),
|
|
28
|
-
commands: Type.Array(CommandSchema
|
|
30
|
+
commands: Type.Array(CommandSchema),
|
|
29
31
|
}, { additionalProperties: false });
|
|
30
32
|
const manifestCheck = TypeCompiler.Compile(ManifestSchema);
|
|
31
33
|
export const findManifest = async (cwd, explicitFile) => {
|
|
@@ -96,6 +98,17 @@ export const resolveCommand = (manifest, manifestPath, selector) => {
|
|
|
96
98
|
};
|
|
97
99
|
};
|
|
98
100
|
const validateManifestSemantics = (manifest, path) => {
|
|
101
|
+
const versionMajor = manifest.version.split('.', 1)[0];
|
|
102
|
+
if (versionMajor !== '1') {
|
|
103
|
+
throw new RunXError(`Unsupported RunX manifest version "${manifest.version}" in ${path}. Supported major version: 1.`);
|
|
104
|
+
}
|
|
105
|
+
const manifestDirectory = dirname(path);
|
|
106
|
+
const scriptsDirectory = resolve(manifestDirectory, manifest.scripts.directory);
|
|
107
|
+
const relativeScriptsDirectory = relative(manifestDirectory, scriptsDirectory);
|
|
108
|
+
invariant(relativeScriptsDirectory !== '' && relativeScriptsDirectory !== '.' && !isAbsolute(relativeScriptsDirectory) && !relativeScriptsDirectory.startsWith('..'), `Scripts directory "${manifest.scripts.directory}" must be a relative subdirectory inside the manifest directory in ${path}.`);
|
|
109
|
+
if (!manifest.groups.public) {
|
|
110
|
+
throw new RunXError(`RunX manifest ${path} must define the default "public" group.`);
|
|
111
|
+
}
|
|
99
112
|
const uids = new Set();
|
|
100
113
|
const selectors = new Set();
|
|
101
114
|
for (const command of manifest.commands) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { RunXUpgradeResult, UpdateResult } from './types.js';
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
export { checkForLatestVersion, listAvailableVersions, uninstallSelf, upgradeSelf, };
|
|
3
|
+
declare const checkForLatestVersion: () => Promise<UpdateResult>;
|
|
4
|
+
declare const listAvailableVersions: () => Promise<string[]>;
|
|
5
|
+
declare const upgradeSelf: (dryRun: boolean) => Promise<RunXUpgradeResult>;
|
|
6
|
+
declare const uninstallSelf: (dryRun: boolean) => Promise<{
|
|
6
7
|
executablePath: string;
|
|
7
8
|
scheduled: boolean;
|
|
8
9
|
dryRun: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"self-management.d.ts","sourceRoot":"","sources":["../source/self-management.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"self-management.d.ts","sourceRoot":"","sources":["../source/self-management.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEjE,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,aAAa,EACb,WAAW,GACZ,CAAA;AAMD,QAAA,MAAM,qBAAqB,QAAa,OAAO,CAAC,YAAY,CAY3D,CAAA;AAED,QAAA,MAAM,qBAAqB,QAAa,OAAO,CAAC,MAAM,EAAE,CAKvD,CAAA;AAED,QAAA,MAAM,WAAW,WAAkB,OAAO,KAAG,OAAO,CAAC,iBAAiB,CAmBrE,CAAA;AAED,QAAA,MAAM,aAAa,WAAkB,OAAO,KAAG,OAAO,CAAC;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAWrH,CAAA"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
1
2
|
import { chmod, rename, rm } from 'node:fs/promises';
|
|
2
3
|
import { basename } from 'node:path';
|
|
3
4
|
import { RunXError } from './errors.js';
|
|
4
5
|
import { readVersion } from './help.js';
|
|
6
|
+
export { checkForLatestVersion, listAvailableVersions, uninstallSelf, upgradeSelf, };
|
|
5
7
|
const repositoryApi = 'https://api.github.com/repos/CGuiho/runx/releases';
|
|
6
|
-
|
|
8
|
+
const checkForLatestVersion = async () => {
|
|
7
9
|
const currentVersion = readVersion();
|
|
8
10
|
try {
|
|
9
11
|
const response = await fetch(`${repositoryApi}/latest`, { headers: { accept: 'application/vnd.github+json' } });
|
|
@@ -18,14 +20,14 @@ export const checkForLatestVersion = async () => {
|
|
|
18
20
|
return { currentVersion, latestVersion: currentVersion, updateAvailable: false };
|
|
19
21
|
}
|
|
20
22
|
};
|
|
21
|
-
|
|
23
|
+
const listAvailableVersions = async () => {
|
|
22
24
|
const response = await fetch(`${repositoryApi}?per_page=20`, { headers: { accept: 'application/vnd.github+json' } });
|
|
23
25
|
if (!response.ok)
|
|
24
26
|
throw new RunXError(`Could not retrieve RunX releases: HTTP ${response.status}`);
|
|
25
27
|
const releases = await response.json();
|
|
26
28
|
return releases.map((release) => release.tag_name.replace(/^@guiho\/runx@/, '').replace(/^v/, ''));
|
|
27
29
|
};
|
|
28
|
-
|
|
30
|
+
const upgradeSelf = async (dryRun) => {
|
|
29
31
|
const executablePath = requireNativeExecutable();
|
|
30
32
|
const update = await checkForLatestVersion();
|
|
31
33
|
const upToDate = !update.updateAvailable && update.latestVersion === update.currentVersion && Boolean(update.url);
|
|
@@ -43,10 +45,10 @@ export const upgradeSelf = async (dryRun) => {
|
|
|
43
45
|
await rename(temporaryPath, executablePath);
|
|
44
46
|
return { ...update, executablePath, scheduled: false, upToDate: false };
|
|
45
47
|
}
|
|
46
|
-
|
|
47
|
-
return { ...update, executablePath, scheduled:
|
|
48
|
+
await replaceWindowsExecutable(temporaryPath, executablePath, update.latestVersion);
|
|
49
|
+
return { ...update, executablePath, scheduled: false, upToDate: false };
|
|
48
50
|
};
|
|
49
|
-
|
|
51
|
+
const uninstallSelf = async (dryRun) => {
|
|
50
52
|
const executablePath = requireNativeExecutable();
|
|
51
53
|
if (dryRun)
|
|
52
54
|
return { executablePath, scheduled: false, dryRun: true };
|
|
@@ -66,10 +68,65 @@ const requireNativeExecutable = () => {
|
|
|
66
68
|
throw new RunXError('Self-management requires a native RunX executable installed from a GitHub release.');
|
|
67
69
|
return executablePath;
|
|
68
70
|
};
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
71
|
+
const replaceWindowsExecutable = async (temporaryPath, executablePath, expectedVersion) => {
|
|
72
|
+
const backupPath = `${executablePath}.old`;
|
|
73
|
+
let originalMoved = false;
|
|
74
|
+
try {
|
|
75
|
+
await rm(backupPath, { force: true });
|
|
76
|
+
await rename(executablePath, backupPath);
|
|
77
|
+
originalMoved = true;
|
|
78
|
+
await rename(temporaryPath, executablePath);
|
|
79
|
+
await verifyExecutableVersion(executablePath, expectedVersion);
|
|
80
|
+
await cleanupWindowsBackup(backupPath);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const replacementFailure = errorMessage(error);
|
|
84
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
85
|
+
if (!originalMoved) {
|
|
86
|
+
throw new RunXError(`Could not replace the Windows RunX executable at ${executablePath}: ${replacementFailure}`);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await rm(executablePath, { force: true });
|
|
90
|
+
await rename(backupPath, executablePath);
|
|
91
|
+
}
|
|
92
|
+
catch (rollbackError) {
|
|
93
|
+
throw new RunXError(`Windows RunX upgrade failed at ${executablePath}: ${replacementFailure}. Automatic rollback also failed: ${errorMessage(rollbackError)}`);
|
|
94
|
+
}
|
|
95
|
+
throw new RunXError(`Windows RunX upgrade failed; the previous executable was restored at ${executablePath}: ${replacementFailure}`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const verifyExecutableVersion = async (executablePath, expectedVersion) => {
|
|
99
|
+
const verification = Bun.spawn([executablePath, '--version'], { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' });
|
|
100
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
101
|
+
new Response(verification.stdout).text(),
|
|
102
|
+
new Response(verification.stderr).text(),
|
|
103
|
+
verification.exited,
|
|
104
|
+
]);
|
|
105
|
+
const actualVersion = stdout.trim();
|
|
106
|
+
if (exitCode !== 0)
|
|
107
|
+
throw new Error(`replacement exited with code ${exitCode}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);
|
|
108
|
+
if (actualVersion !== expectedVersion)
|
|
109
|
+
throw new Error(`replacement reported version ${actualVersion || '<empty>'}; expected ${expectedVersion}`);
|
|
110
|
+
};
|
|
111
|
+
const cleanupWindowsBackup = async (backupPath) => {
|
|
112
|
+
try {
|
|
113
|
+
await rm(backupPath, { force: true });
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
scheduleWindowsBackupCleanup(backupPath);
|
|
117
|
+
};
|
|
118
|
+
const scheduleWindowsBackupCleanup = (backupPath) => {
|
|
119
|
+
const command = 'for ($attempt = 0; $attempt -lt 300; $attempt += 1) { if (-not (Test-Path -LiteralPath $env:RUNX_BACKUP_PATH)) { exit 0 }; try { Remove-Item -LiteralPath $env:RUNX_BACKUP_PATH -Force -ErrorAction Stop; exit 0 } catch { Start-Sleep -Milliseconds 100 } }; exit 1';
|
|
120
|
+
const encodedCommand = Buffer.from(command, 'utf16le').toString('base64');
|
|
121
|
+
Bun.spawn(['cmd.exe', '/d', '/s', '/c', `powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ${encodedCommand}`], {
|
|
122
|
+
detached: true,
|
|
123
|
+
env: { ...process.env, RUNX_BACKUP_PATH: backupPath },
|
|
124
|
+
stdout: 'ignore',
|
|
125
|
+
stderr: 'ignore',
|
|
126
|
+
stdin: 'ignore',
|
|
127
|
+
});
|
|
72
128
|
};
|
|
129
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
73
130
|
const compareVersions = (left, right) => {
|
|
74
131
|
const parse = (value) => value.split(/[.+-]/).map((part) => Number.parseInt(part, 10) || 0);
|
|
75
132
|
const a = parse(left);
|
package/package.json
CHANGED