@fastang/cli 0.0.0-stage → 0.1.0-alpha.3
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +38 -2
- package/bin/fastang.mjs +23 -0
- package/dist/commands/project.d.ts +13 -0
- package/dist/commands/project.js +140 -0
- package/dist/process.d.ts +8 -0
- package/dist/process.js +44 -0
- package/package.json +41 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0-alpha.3
|
|
4
|
+
|
|
5
|
+
- Standalone lightweight `fastang` and `fastang-cli` commands.
|
|
6
|
+
- Create projects with the matching generator release.
|
|
7
|
+
- Plan and apply npm core updates, including apps using older framework versions.
|
|
8
|
+
- Forward development, build, generation and route inspection to the local core.
|
|
9
|
+
|
|
10
|
+
- Compare installed peers and Node before updates; block incompatible installs.
|
|
11
|
+
- Skip identical versions and require explicit permission for downgrades.
|
|
12
|
+
- Forward termination signals to local server processes.
|
|
13
|
+
|
|
14
|
+
- Check manifest, lockfile and installed core consistency before applying updates.
|
|
15
|
+
- Use one process runner for npm and local runtime commands.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dario Chiapperini
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @fastang/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Lightweight FastAng CLI. Available starting with `0.1.0-alpha.3`:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install -g @fastang/cli@alpha
|
|
7
|
+
fastang new my-app --install
|
|
8
|
+
cd my-app
|
|
9
|
+
fastang dev
|
|
10
|
+
fastang update --to alpha
|
|
11
|
+
fastang update --to alpha --yes
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Without a global install, use `npx --package=@fastang/cli@alpha fastang new my-app`.
|
|
15
|
+
`new` uses the generator matching the CLI release. `dev`, `build`, `generate` and
|
|
16
|
+
`routes` execute the app's installed core, preserving its version and arguments.
|
|
17
|
+
No Angular, Vite or Fastify dependencies are installed with this CLI.
|
|
18
|
+
Its only runtime dependency is semver for version comparisons.
|
|
19
|
+
|
|
20
|
+
`update` also works with older cores: it runs in the standalone CLI and updates
|
|
21
|
+
the app's core dependency. It defaults to a plan. `--yes` installs the resolved
|
|
22
|
+
exact version with strict peer and Node engine checks. Source migrations remain
|
|
23
|
+
manual; commit your app first, review the release instructions and run your tests.
|
|
24
|
+
Use `--to latest` for the npm latest tag or `--to alpha` for the alpha channel.
|
|
25
|
+
|
|
26
|
+
Inside npm scripts and with `npx fastang`, the app's local binary takes precedence.
|
|
27
|
+
For an older project, explicitly select the standalone CLI without a bin conflict:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx --package=@fastang/cli@alpha fastang-cli update --to alpha
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The alias `fastang-cli` provides the same commands as `fastang`. Avoid installing
|
|
34
|
+
this package alongside core as an app dependency; a global install or npm exec
|
|
35
|
+
keeps the two executables separate.
|
|
36
|
+
|
|
37
|
+
The plan compares installed versions and Node with target requirements. Applying
|
|
38
|
+
is blocked on incompatibility. An identical installed version is a no-op;
|
|
39
|
+
downgrades require `--allow-downgrade` explicitly. Source migrations remain manual.
|
package/bin/fastang.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { ProcessError, runProcess } from '../dist/process.js';
|
|
4
|
+
import { cliVersion, installedPackage, newProject, updateProject } from '../dist/commands/project.js';
|
|
5
|
+
|
|
6
|
+
const [command, ...args] = process.argv.slice(2);
|
|
7
|
+
try {
|
|
8
|
+
if (command === 'new') await newProject(args);
|
|
9
|
+
else if (command === 'update') await updateProject(args);
|
|
10
|
+
else if (command === '--version' || command === '-v') console.log(await cliVersion());
|
|
11
|
+
else if (!command || command === '--help' || command === '-h') {
|
|
12
|
+
console.log('Usage: fastang <new|update|dev|build|generate|routes>\n new <directory> [--install]\n update --to <version|tag> [--dry-run | --yes] [--allow-downgrade]\n dev, build, generate, routes: use the core installed in your app');
|
|
13
|
+
} else if (['dev', 'build', 'generate', 'routes'].includes(command)) {
|
|
14
|
+
const pkg = await installedPackage('@fastang/core', process.cwd());
|
|
15
|
+
if (!pkg) throw new Error('No local @fastang/core found. Run this command inside an installed FastAng app.');
|
|
16
|
+
if (typeof pkg.manifest.bin?.fastang !== 'string') throw new Error('The local core has no fastang executable.');
|
|
17
|
+
const bin = resolve(pkg.directory, pkg.manifest.bin.fastang);
|
|
18
|
+
await runProcess(process.execPath, [bin, command, ...args]);
|
|
19
|
+
} else throw new Error(`Unknown command: ${command}`);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error(`fastang: ${error instanceof Error ? error.message : error}`);
|
|
22
|
+
process.exitCode = error instanceof ProcessError ? error.code : 1;
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type NpmRunner = (args: string[], capture?: boolean) => Promise<string>;
|
|
2
|
+
export declare const runNpm: NpmRunner;
|
|
3
|
+
export declare function cliVersion(): Promise<string>;
|
|
4
|
+
export declare function newProject(args: string[], npm?: NpmRunner): Promise<void>;
|
|
5
|
+
export declare function installedPackage(name: string, directory: string): Promise<{
|
|
6
|
+
directory: string;
|
|
7
|
+
manifest: Record<string, any>;
|
|
8
|
+
} | undefined>;
|
|
9
|
+
export declare function installedVersion(name: string, directory: string): Promise<string | undefined>;
|
|
10
|
+
export declare function updateProject(args: string[], npm?: NpmRunner, directory?: string, log?: {
|
|
11
|
+
(...data: any[]): void;
|
|
12
|
+
(message?: any, ...optionalParams: any[]): void;
|
|
13
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { lt, satisfies, valid, validRange } from 'semver';
|
|
4
|
+
import { runProcess } from "../process.js";
|
|
5
|
+
export const runNpm = async (args, capture = false) => {
|
|
6
|
+
const windows = process.platform === 'win32';
|
|
7
|
+
if (windows && args.some(arg => /[&|<>^%!"\r\n]/.test(arg)))
|
|
8
|
+
throw new Error('Unsupported shell characters in npm arguments on Windows.');
|
|
9
|
+
return runProcess(windows ? 'npm.cmd' : 'npm', windows ? args.map(arg => `"${arg}"`) : args, { capture, shell: windows });
|
|
10
|
+
};
|
|
11
|
+
export async function cliVersion() {
|
|
12
|
+
const manifest = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
13
|
+
return manifest.version;
|
|
14
|
+
}
|
|
15
|
+
export async function newProject(args, npm = runNpm) {
|
|
16
|
+
// A release always uses its matching generator; existing destinations are rejected there.
|
|
17
|
+
await npm(['exec', '--yes', `--package=create-fastang@${await cliVersion()}`, '--', 'create-fastang', ...args]);
|
|
18
|
+
}
|
|
19
|
+
export async function installedPackage(name, directory) {
|
|
20
|
+
let current = resolve(directory);
|
|
21
|
+
while (true) {
|
|
22
|
+
const root = join(current, 'node_modules', name);
|
|
23
|
+
try {
|
|
24
|
+
const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
25
|
+
if (manifest.name === name)
|
|
26
|
+
return { directory: root, manifest };
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== 'ENOENT')
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
const parent = dirname(current);
|
|
33
|
+
if (parent === current)
|
|
34
|
+
return undefined;
|
|
35
|
+
current = parent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function installedVersion(name, directory) {
|
|
39
|
+
const pkg = await installedPackage(name, directory);
|
|
40
|
+
return pkg && valid(pkg.manifest.version) ? pkg.manifest.version : undefined;
|
|
41
|
+
}
|
|
42
|
+
export async function updateProject(args, npm = runNpm, directory = process.cwd(), log = console.log) {
|
|
43
|
+
if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
|
|
44
|
+
log('Usage: fastang update --to <version|tag> [--dry-run | --yes] [--allow-downgrade]');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let target;
|
|
48
|
+
let apply = false;
|
|
49
|
+
let dryRun = false;
|
|
50
|
+
let allowDowngrade = false;
|
|
51
|
+
for (let i = 0; i < args.length; i++) {
|
|
52
|
+
const arg = args[i];
|
|
53
|
+
if (arg === '--to' && !target)
|
|
54
|
+
target = args[++i];
|
|
55
|
+
else if (arg === '--allow-downgrade' && !allowDowngrade)
|
|
56
|
+
allowDowngrade = true;
|
|
57
|
+
else if (arg === '--yes' && !apply)
|
|
58
|
+
apply = true;
|
|
59
|
+
else if (arg === '--dry-run' && !dryRun)
|
|
60
|
+
dryRun = true;
|
|
61
|
+
else
|
|
62
|
+
throw new Error('Usage: fastang update --to <version|tag> [--dry-run | --yes] [--allow-downgrade]');
|
|
63
|
+
}
|
|
64
|
+
if (!target || !/^[a-zA-Z0-9][a-zA-Z0-9.+-]*$/.test(target) || (apply && dryRun)) {
|
|
65
|
+
throw new Error('Usage: fastang update --to <version|tag> [--dry-run | --yes] [--allow-downgrade]');
|
|
66
|
+
}
|
|
67
|
+
const project = JSON.parse(await readFile(resolve(directory, 'package.json'), 'utf8'));
|
|
68
|
+
if (project.workspaces)
|
|
69
|
+
throw new Error('Run update in an individual app, not a workspace root. Update workspace dependencies manually.');
|
|
70
|
+
if (project.packageManager && !project.packageManager.startsWith('npm@'))
|
|
71
|
+
throw new Error('fastang update currently supports npm projects only.');
|
|
72
|
+
const section = project.dependencies?.['@fastang/core'] ? 'dependencies' : project.devDependencies?.['@fastang/core'] ? 'devDependencies' : undefined;
|
|
73
|
+
if (!section)
|
|
74
|
+
throw new Error('This project does not declare @fastang/core. Run the command from your app directory.');
|
|
75
|
+
const response = JSON.parse(await npm(['view', `@fastang/core@${target}`, 'version', 'engines', 'peerDependencies', 'peerDependenciesMeta', '--json'], true));
|
|
76
|
+
const metadata = Array.isArray(response) && response.length === 1 ? response[0] : response;
|
|
77
|
+
if (!metadata || typeof metadata.version !== 'string' || !/^\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/.test(metadata.version))
|
|
78
|
+
throw new Error('npm did not return a single release version.');
|
|
79
|
+
const current = await installedVersion('@fastang/core', directory);
|
|
80
|
+
const declared = project[section]['@fastang/core'];
|
|
81
|
+
const problems = [];
|
|
82
|
+
let lock;
|
|
83
|
+
try {
|
|
84
|
+
lock = JSON.parse(await readFile(resolve(directory, 'package-lock.json'), 'utf8'));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error.code !== 'ENOENT')
|
|
88
|
+
throw error;
|
|
89
|
+
problems.push('package-lock.json is missing. Run npm install and review the lockfile first.');
|
|
90
|
+
}
|
|
91
|
+
const locked = lock?.packages?.['node_modules/@fastang/core']?.version;
|
|
92
|
+
const lockedDeclaration = lock?.packages?.['']?.[section]?.['@fastang/core'];
|
|
93
|
+
log(`@fastang/core: declared ${declared}; locked ${locked ?? 'unknown'}; installed ${current ?? 'missing'}; target ${metadata.version}`);
|
|
94
|
+
if (lock && ![2, 3].includes(lock.lockfileVersion))
|
|
95
|
+
problems.push('Unsupported lockfile format. Regenerate with a supported npm version.');
|
|
96
|
+
if (lock && lockedDeclaration !== declared)
|
|
97
|
+
problems.push('Manifest and lockfile declarations differ. Run npm install and review the diff first.');
|
|
98
|
+
if (lock && (!valid(locked) || locked !== current))
|
|
99
|
+
problems.push('Lockfile and installed core differ. Run npm ci first.');
|
|
100
|
+
if (current && validRange(declared) && !satisfies(current, declared))
|
|
101
|
+
problems.push('Installed core does not satisfy the manifest declaration. Reconcile the manifest and installation first.');
|
|
102
|
+
const downgrade = current && lt(metadata.version, current);
|
|
103
|
+
if (downgrade)
|
|
104
|
+
log('DOWNGRADE: target is older than the installed core.');
|
|
105
|
+
if (!current)
|
|
106
|
+
problems.push('Cannot determine installed core. Run npm install first.');
|
|
107
|
+
if (metadata.engines?.node) {
|
|
108
|
+
const ok = satisfies(process.versions.node, metadata.engines.node);
|
|
109
|
+
log(`Node ${process.versions.node}: ${ok ? 'OK' : 'INCOMPATIBLE'} (requires ${metadata.engines.node})`);
|
|
110
|
+
if (!ok)
|
|
111
|
+
problems.push('Node does not satisfy the target engine requirement.');
|
|
112
|
+
}
|
|
113
|
+
for (const [name, range] of Object.entries(metadata.peerDependencies ?? {})) {
|
|
114
|
+
const version = await installedVersion(name, directory);
|
|
115
|
+
const optional = metadata.peerDependenciesMeta?.[name]?.optional === true;
|
|
116
|
+
const ok = version ? satisfies(version, range) : optional;
|
|
117
|
+
log(`${name}: ${version ?? 'not installed'} — ${ok ? 'OK' : 'INCOMPATIBLE'} (requires ${range}${optional ? ', optional' : ''})`);
|
|
118
|
+
if (!ok)
|
|
119
|
+
problems.push(`${name} must satisfy ${range}.`);
|
|
120
|
+
}
|
|
121
|
+
log('Read the release migration notes and commit your app and lockfile first. App source files are not migrated.');
|
|
122
|
+
if (current === metadata.version && !problems.length) {
|
|
123
|
+
log('Already installed. No changes.');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!apply) {
|
|
127
|
+
for (const problem of problems)
|
|
128
|
+
log(problem);
|
|
129
|
+
log(`Plan only. Apply with: fastang-cli update --to ${metadata.version} --yes${downgrade ? ' --allow-downgrade' : ''}`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (downgrade && !allowDowngrade)
|
|
133
|
+
throw new Error('Downgrade blocked. Use --allow-downgrade explicitly.');
|
|
134
|
+
if (problems.length)
|
|
135
|
+
throw new Error(problems.join('\n'));
|
|
136
|
+
if (resolve(directory) !== resolve(process.cwd()))
|
|
137
|
+
throw new Error('Apply must run from the app directory.');
|
|
138
|
+
await npm(['install', section === 'devDependencies' ? '--save-dev' : '--save-prod', '--save-exact', '--engine-strict', '--strict-peer-deps', `@fastang/core@${metadata.version}`]);
|
|
139
|
+
log('Dependency updated. Review the diff, apply any documented migrations, then run your build and tests.');
|
|
140
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class ProcessError extends Error {
|
|
2
|
+
readonly code: number;
|
|
3
|
+
constructor(code: number, message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare function runProcess(command: string, args: string[], options?: {
|
|
6
|
+
capture?: boolean;
|
|
7
|
+
shell?: boolean;
|
|
8
|
+
}): Promise<string>;
|
package/dist/process.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export class ProcessError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function runProcess(command, args, options = {}) {
|
|
10
|
+
return new Promise((resolveRun, reject) => {
|
|
11
|
+
const child = spawn(command, args, { stdio: options.capture ? ['ignore', 'pipe', 'inherit'] : 'inherit', shell: options.shell ?? false, detached: process.platform !== 'win32' });
|
|
12
|
+
let output = '';
|
|
13
|
+
const forward = (signal) => {
|
|
14
|
+
try {
|
|
15
|
+
if (process.platform !== 'win32' && child.pid)
|
|
16
|
+
process.kill(-child.pid, signal);
|
|
17
|
+
else
|
|
18
|
+
child.kill(signal);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code !== 'ESRCH')
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const interrupt = () => forward('SIGINT');
|
|
26
|
+
const terminate = () => forward('SIGTERM');
|
|
27
|
+
process.on('SIGINT', interrupt);
|
|
28
|
+
process.on('SIGTERM', terminate);
|
|
29
|
+
const cleanup = () => {
|
|
30
|
+
process.removeListener('SIGINT', interrupt);
|
|
31
|
+
process.removeListener('SIGTERM', terminate);
|
|
32
|
+
};
|
|
33
|
+
child.stdout?.on('data', chunk => { output += String(chunk); });
|
|
34
|
+
child.on('error', error => { cleanup(); reject(error); });
|
|
35
|
+
child.on('close', (code, signal) => {
|
|
36
|
+
cleanup();
|
|
37
|
+
const status = code ?? (signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 1);
|
|
38
|
+
if (status === 0)
|
|
39
|
+
resolveRun(output);
|
|
40
|
+
else
|
|
41
|
+
reject(new ProcessError(status, `${command} ${signal ? `terminated by ${signal}` : `exited with code ${status}`}`));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastang/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Lightweight FastAng project CLI.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Dario Chiapperini",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": "^22.22.3 || ^24.15.0 || >=26.0.0"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"fastang": "./bin/fastang.mjs",
|
|
13
|
+
"fastang-cli": "./bin/fastang.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"prepack": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/daclab-dev/fastang.git",
|
|
29
|
+
"directory": "packages/cli"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"tag": "alpha",
|
|
34
|
+
"registry": "https://registry.npmjs.org/"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://fastang.dev",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/daclab-dev/fastang/issues"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"semver": "^7.7.2"
|
|
42
|
+
}
|
|
43
|
+
}
|