@nm-lang/cli 0.5.0 → 0.7.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.
package/README.md CHANGED
@@ -1,135 +1,172 @@
1
- # @nm-lang/cli
2
-
3
- N/M 0.2.0 Preview command-line tools for running, formatting, linting, testing,
4
- exporting and managing local N/M workspaces.
5
-
6
- Package **0.5.0** targets the N/M **0.2.0** language contract.
7
-
8
- ## Install / Kurulum
9
-
10
- Requires Node.js 20 or newer. Release checks use Node.js 22.23.1.
11
-
12
- ```sh
13
- npm install --global @nm-lang/cli
14
- nm --version
15
- ```
16
-
17
- Or run without a global install:
18
-
19
- ```sh
20
- npx --package=@nm-lang/cli nm --version
21
- ```
22
-
23
- An untagged install selects npm's `latest` channel. To pin this version,
24
- append `@0.5.0` to the package name. The N/M language remains
25
- Preview independently of the npm channel. npm installs the matching Core dependency.
26
- No repository checkout or TypeScript build is needed to use a published package.
27
-
28
- Türkçe: Etiketsiz kurulum npm'in `latest` kanalını seçer.
29
- `@0.5.0` ile sürümü sabitleyebilirsiniz. N/M dili, npm kanalından bağımsız
30
- olarak Önizleme durumundadır. Core bağımlılığı otomatik kurulur; depoyu
31
- indirmeniz gerekmez.
32
-
33
- ## First program / İlk program
34
-
35
- Save the following as `bell.nm`:
36
-
37
- ```nm
38
- module npm_bell;
39
- fn main() {
40
- let q = qreg[2];
41
- H(q[0]);
42
- CNOT(q[0], q[1]);
43
- let a = measure(q[0]);
44
- let b = measure(q[1]);
45
- return (a, b);
46
- }
47
- ```
48
-
49
- ```sh
50
- nm run bell.nm --json
51
- ```
52
-
53
- The program creates and measures a Bell pair using a local simulator.
54
- Türkçe: Kodu `bell.nm` olarak kaydedip komutla çalıştırın. Sonuç yerel
55
- simülasyondan gelir; gerçek kuantum donanımı kanıtı değildir.
56
-
57
- ## Language options / Dil seçenekleri
58
-
59
- ### Selected tests and CI reports / Test seçimi ve CI raporları
60
-
61
- The CLI provides test selection, watch mode and JSON, JUnit and SARIF
62
- reports for CI integrations.
63
-
64
- Türkçe: CLI test seçimini, izleme modunu ve CI entegrasyonları için
65
- JSON, JUnit ve SARIF raporlarını içerir.
66
-
67
- ```sh
68
- nm test main.nm --name "Bell contract" --json
69
- nm test main.nm --id nmtest-0123456789abcdef
70
- nm test main.nm --report-format junit --output results.xml
71
- nm test main.nm --report-format sarif --output results.sarif
72
- nm test main.nm --watch --json
73
- ```
74
-
75
- `--name` matches the exact declared name; repeat `--name` or `--id` to select a
76
- union. Copy real IDs from a JSON report for the same input path and source lines.
77
- Selection happens before execution, preserving surrounding setup and original
78
- source locations; unknown/stale IDs or names fail with `NM-TEST-SELECT-001`.
79
- Direct selection of a nested check fails with `NM-TEST-SELECT-002`; select its
80
- top-level enclosing check. Source syntax and capability validation still cover
81
- the complete file, including its original register allocation and qubit limits.
82
- Empty suites and failed execution exit 1.
83
-
84
- `--report-format json|junit|sarif` writes that format to stdout. `--output` also
85
- replaces the explicitly named `.json`, `.xml` or `.sarif` report on each run;
86
- parent directories must already exist. The default output remains human readable.
87
- `--json` cannot be combined with a non-JSON report format.
88
-
89
- `--watch` is available in the Node command-line host. It runs immediately, then
90
- debounces source and workspace-manifest changes, including imported modules,
91
- clears the workspace cache and queues runs without overlap. Ctrl+C/SIGTERM closes
92
- watchers. With `--json`, stdout is NDJSON: each `event:"run"` has a sequence,
93
- exitCode, report (or null for invocation errors) and diagnostics; lifecycle errors
94
- and stopping also have event objects. JUnit/SARIF watch mode requires `--output`
95
- and prints run summaries while replacing the report file. The exit status follows
96
- the latest run; signal termination uses 130/143.
97
-
98
- Türkçe: Tam test adı veya mevcut rapordaki kimlikle seçim yapın; seçenekler
99
- tekrarlanabilir. Seçilmeyen test blokları yürütülmez, ortak kurulum korunur.
100
- İç içe kontrolleri doğrudan seçmek yerine üst kontrolü seçin. Kaynağın tamamındaki
101
- kayıt tahsisi ve qubit sınırları seçimden sonra da geçerlidir. İzleme modu bağımlı
102
- `.nm` dosyaları ve manifest değişince yeniden çalışır. `--output` belirtilen rapor
103
- dosyasını her çalıştırmada yeniler.
104
-
105
- ### Toolchain doctor / Araç zinciri kontrolü
106
-
107
- The `0.3.0` CLI provides `nm doctor --json` and `nm doctor --locale tr`.
108
- It compares the loaded CLI/Core and locally resolvable LSP/MCP packages, reporting
109
- package, language and protocol versions separately. Missing optional packages and
110
- the VS Code installation remain unobserved; this is not a check of running servers.
111
- Exit code 1 means an observed mismatch or failed package probe; 0 may still mean
112
- partial coverage. The report format is `nm-toolchain-doctor` version `0.1`.
113
-
114
- Türkçe: `nm doctor` yüklenen CLI/Core ile yerelden çözümlenebilen LSP/MCP
115
- paketlerini karşılaştırır. Paket, dil ve protokol sürümleri ayrı değerlendirilir.
116
- Gözlenmeyen isteğe bağlı araçlar ve VS Code kurulumu doğrulanmış sayılmaz.
117
- Çıkış kodu 1 uyuşmazlık veya paket sorgusu hatasıdır; 0 kısmi gözlem anlamına da
118
- gelebilir. Bu komutlar `0.3.0` paketinin parçasıdır.
119
-
120
- New CLI invocations enable the default language profile: classical functions,
121
- typed values/Result, module visibility, parameter groups and compute/apply.
122
- Use `--legacy-features` for the previous opt-in behavior. Other experimental
123
- features still require their own flags; export/backend limits remain enforced.
124
-
125
- Türkçe: Yeni CLI çağrılarında varsayılan dil profili açıktır. Eski davranış için
126
- `--legacy-features` kullanın. Diğer deneysel özellikler ayrıca açılmalıdır.
127
-
128
- - [CLI documentation](https://quantumsoftware.tech/en/docs/nm-language/cli)
129
- - [Türkçe CLI rehberi](https://quantumsoftware.tech/tr/docs/nm-language/cli)
130
- - [Support / Destek](https://quantumsoftware.tech/en/#contact)
131
-
132
-
1
+ # @nm-lang/cli
2
+
3
+ N/M 0.2.0 Preview command-line tools for running, formatting, linting, testing,
4
+ exporting and managing local N/M workspaces.
5
+
6
+ Package **0.7.0** targets the N/M **0.2.0** language contract.
7
+
8
+ ## 0.7.0 training numerics / Eğitim sayısal işlemleri
9
+
10
+ Core 0.7.0 adds first-order Hermitian exponential H/time derivatives, finite/analytic block QNG, shared-evaluation gradient covariance, deterministic stateful GD/momentum/Adam/QNG checkpoints and past-covariance shot allocation. Language 0.2.0, explicit permissions and scientific limits remain. Analytic QNG is initially flat/noiseless; sampled checkpoint continuation is rejected. Covariance allocation is a heuristic within the existing total budget.
11
+
12
+ Türkçe: Matris üstel türevi, blok/analitik QNG, gradyan kovaryansı, deterministik durumlu devam ve önceki kovaryansla ölçüm dağılımı eklendi. İzinler ve sınırlar korunur; kayıt izin vermez, sonlu ölçümle devam desteklenmez ve daha düşük hata garantisi yoktur. Normal editör Run ayrı kalır.
13
+
14
+ See [Quantum AI](https://quantumsoftware.tech/en/quantum-ai) and the [public documentation](https://quantumsoftware.tech/en/docs/nm-language). Registry availability is separate from this package version.
15
+
16
+ ## Install / Kurulum
17
+
18
+ Requires Node.js 20 or newer. Release checks use Node.js 22.23.1.
19
+
20
+ ```sh
21
+ npm install --global @nm-lang/cli
22
+ nm --version
23
+ ```
24
+
25
+ Or run without a global install:
26
+
27
+ ```sh
28
+ npx --package=@nm-lang/cli nm --version
29
+ ```
30
+
31
+ An untagged install selects npm's `latest` channel. To pin this version,
32
+ append `@0.7.0` to the package name. The N/M language remains
33
+ Preview independently of the npm channel. npm installs the matching Core dependency.
34
+ No repository checkout or TypeScript build is needed to use a published package.
35
+
36
+ Türkçe: Etiketsiz kurulum npm'in `latest` kanalını seçer.
37
+ `@0.7.0` ile sürümü sabitleyebilirsiniz. N/M dili, npm kanalından bağımsız
38
+ olarak Önizleme durumundadır. Core bağımlılığı otomatik kurulur; depoyu
39
+ indirmeniz gerekmez.
40
+
41
+ ## First program / İlk program
42
+
43
+ Save the following as `bell.nm`:
44
+
45
+ ```nm
46
+ module npm_bell;
47
+ fn main() {
48
+ let q = qreg[2];
49
+ H(q[0]);
50
+ CNOT(q[0], q[1]);
51
+ let a = measure(q[0]);
52
+ let b = measure(q[1]);
53
+ return (a, b);
54
+ }
55
+ ```
56
+
57
+ ```sh
58
+ nm run bell.nm --json
59
+ ```
60
+
61
+ The program creates and measures a Bell pair using a local simulator.
62
+ Türkçe: Kodu `bell.nm` olarak kaydedip komutla çalıştırın. Sonuç yerel
63
+ simülasyondan gelir; gerçek kuantum donanımı kanıtı değildir.
64
+
65
+ ## 0.6.0 project templates / proje şablonları
66
+
67
+ Create a complete workspace with a helper module, test and manifest:
68
+
69
+ ```sh
70
+ nm init bell-project --template bell --name bell-project --locale en
71
+ cd bell-project
72
+ nm run src/main.nm --json
73
+ nm test tests/main.test.nm --json
74
+ ```
75
+
76
+ Templates are `bell`, `parametric-training` and `numerics`. Use `--locale tr`
77
+ for a Turkish project README; the canonical program is shared. The numerics
78
+ README includes the explicit classical-function/typed-value flags needed by
79
+ its `solve` and `eigvecsh` example. Creation accepts a new/empty real directory,
80
+ preserves existing files and does not grant features. There is no project `--force`.
81
+ The Node-only writer is exported as `@nm-lang/cli/project-create`.
82
+
83
+ Core 0.6.0 also adds QR `lstsq`, real solve derivatives with factor reuse,
84
+ and Hermitian eigenvectors. The bundled Python adapter supports optional
85
+ IPython `%%nm` cells over this CLI; it does not install IPython automatically
86
+ or publish a separate PyPI package. Trusted commands and caller grants remain explicit.
87
+
88
+ Türkçe: Bell, parametrik eğitim ve sayısal hesaplama için gerçek proje,
89
+ yardımcı modül, test ve manifest oluşturulur. `--locale tr` Türkçe README üretir.
90
+ Dolu klasörler ve güvensiz yollar reddedilir; kullanıcı dosyaları korunur.
91
+ Sayısal komutlar gereken bayrakları README'de gösterir. Python/Jupyter adaptörü
92
+ aynı CLI'yi kullanır; ayrıca yorumlayıcı indirmez veya izin açmaz.
93
+
94
+ ## Language options / Dil seçenekleri
95
+
96
+ ### Selected tests and CI reports / Test seçimi ve CI raporları
97
+
98
+ The CLI provides test selection, watch mode and JSON, JUnit and SARIF
99
+ reports for CI integrations.
100
+
101
+ Türkçe: CLI test seçimini, izleme modunu ve CI entegrasyonları için
102
+ JSON, JUnit ve SARIF raporlarını içerir.
103
+
104
+ ```sh
105
+ nm test main.nm --name "Bell contract" --json
106
+ nm test main.nm --id nmtest-0123456789abcdef
107
+ nm test main.nm --report-format junit --output results.xml
108
+ nm test main.nm --report-format sarif --output results.sarif
109
+ nm test main.nm --watch --json
110
+ ```
111
+
112
+ `--name` matches the exact declared name; repeat `--name` or `--id` to select a
113
+ union. Copy real IDs from a JSON report for the same input path and source lines.
114
+ Selection happens before execution, preserving surrounding setup and original
115
+ source locations; unknown/stale IDs or names fail with `NM-TEST-SELECT-001`.
116
+ Direct selection of a nested check fails with `NM-TEST-SELECT-002`; select its
117
+ top-level enclosing check. Source syntax and capability validation still cover
118
+ the complete file, including its original register allocation and qubit limits.
119
+ Empty suites and failed execution exit 1.
120
+
121
+ `--report-format json|junit|sarif` writes that format to stdout. `--output` also
122
+ replaces the explicitly named `.json`, `.xml` or `.sarif` report on each run;
123
+ parent directories must already exist. The default output remains human readable.
124
+ `--json` cannot be combined with a non-JSON report format.
125
+
126
+ `--watch` is available in the Node command-line host. It runs immediately, then
127
+ debounces source and workspace-manifest changes, including imported modules,
128
+ clears the workspace cache and queues runs without overlap. Ctrl+C/SIGTERM closes
129
+ watchers. With `--json`, stdout is NDJSON: each `event:"run"` has a sequence,
130
+ exitCode, report (or null for invocation errors) and diagnostics; lifecycle errors
131
+ and stopping also have event objects. JUnit/SARIF watch mode requires `--output`
132
+ and prints run summaries while replacing the report file. The exit status follows
133
+ the latest run; signal termination uses 130/143.
134
+
135
+ Türkçe: Tam test adı veya mevcut rapordaki kimlikle seçim yapın; seçenekler
136
+ tekrarlanabilir. Seçilmeyen test blokları yürütülmez, ortak kurulum korunur.
137
+ İç içe kontrolleri doğrudan seçmek yerine üst kontrolü seçin. Kaynağın tamamındaki
138
+ kayıt tahsisi ve qubit sınırları seçimden sonra da geçerlidir. İzleme modu bağımlı
139
+ `.nm` dosyaları ve manifest değişince yeniden çalışır. `--output` belirtilen rapor
140
+ dosyasını her çalıştırmada yeniler.
141
+
142
+ ### Toolchain doctor / Araç zinciri kontrolü
143
+
144
+ The `0.3.0` CLI provides `nm doctor --json` and `nm doctor --locale tr`.
145
+ It compares the loaded CLI/Core and locally resolvable LSP/MCP packages, reporting
146
+ package, language and protocol versions separately. Missing optional packages and
147
+ the VS Code installation remain unobserved; this is not a check of running servers.
148
+ Exit code 1 means an observed mismatch or failed package probe; 0 may still mean
149
+ partial coverage. The report format is `nm-toolchain-doctor` version `0.1`.
150
+
151
+ Türkçe: `nm doctor` yüklenen CLI/Core ile yerelden çözümlenebilen LSP/MCP
152
+ paketlerini karşılaştırır. Paket, dil ve protokol sürümleri ayrı değerlendirilir.
153
+ Gözlenmeyen isteğe bağlı araçlar ve VS Code kurulumu doğrulanmış sayılmaz.
154
+ Çıkış kodu 1 uyuşmazlık veya paket sorgusu hatasıdır; 0 kısmi gözlem anlamına da
155
+ gelebilir. Bu komutlar `0.3.0` paketinin parçasıdır.
156
+
157
+ New CLI invocations enable the default language profile: classical functions,
158
+ typed values/Result, module visibility, parameter groups and compute/apply.
159
+ Use `--legacy-features` for the previous opt-in behavior. Other experimental
160
+ features still require their own flags; export/backend limits remain enforced.
161
+
162
+ Türkçe: Yeni CLI çağrılarında varsayılan dil profili açıktır. Eski davranış için
163
+ `--legacy-features` kullanın. Diğer deneysel özellikler ayrıca açılmalıdır.
164
+
165
+ - [CLI documentation](https://quantumsoftware.tech/en/docs/nm-language/cli)
166
+ - [Türkçe CLI rehberi](https://quantumsoftware.tech/tr/docs/nm-language/cli)
167
+ - [Support / Destek](https://quantumsoftware.tech/en/#contact)
168
+
169
+
133
170
  ## 0.5.0: Trainable evolution / Eğitilebilir evrim
134
171
 
135
172
  This package adds scalar-parameter evolution time and Hamiltonian coefficients,
@@ -149,52 +186,52 @@ son maliyet hassasiyetini destekler. Ölçüm tahmini bilgi amaçlıdır; ek öl
149
186
  The N/M language contract remains 0.2.0; package and artifact versions are separate.
150
187
  N/M dil sözleşmesi 0.2.0 olarak kalır; paket ve dosya biçimi sürümleri ayrıdır.
151
188
 
152
- ## Hamiltonian 0.4.0 / Hamiltonian 0.4.0
153
-
154
- Version 0.4.0 includes Hamiltonian evolution and shared-shot QWC expectations.
155
- Earlier releases do not include these additions.
156
- Both `hamiltonianEvolution` and `measurementGrouping` require explicit permission.
157
- Source printing and N/M IR v0.5 preserve original source, parser context, seed and
158
- shot plans. Imports rebuild with caller permissions; stored flags never grant them.
159
- QASM/framework export and source optimization/transpilation reject these semantics.
160
-
161
- CLI: `--experimental-hamiltonian-evolution`, `--experimental-measurement-grouping`,
162
- `--from-ir` for a saved v0.5 IR, `--locale en|tr` for diagnostics. MCP: the matching
163
- `experimentalHamiltonianEvolution` / `experimentalMeasurementGrouping` booleans,
164
- `nm_export` target `ir` and `nm_import_ir`. LSP initialization uses
165
- `initializationOptions.nm.experimental`; VS Code uses
166
- `nm.experimental.hamiltonianEvolution` / `nm.experimental.measurementGrouping`.
167
- The editor offers completion/hover, closed-default diagnostics and original
168
- source columns for generated gates in the recorded debugger.
169
-
170
- Türkçe: 0.4.0 bu geliştirmeleri içerir; iki yeni izin varsayılan kapalıdır.
171
- Yazdırma/IR özgün kaynağı ve ortak ölçüm bütçesini korur; içe aktarım
172
- kaydı çalıştırma yetkisi saymaz. CLI/MCP/LSP/Monaco/VS Code aynı izinleri taşır;
173
- tanılar ve ayarlar TR/EN destekler. TR/EN laboratuvarı sitede erişilebilirdir.
174
- N/M dil sözleşmesi 0.2.0 ve ön izleme olgunluğu korunur.
175
-
176
- [Release notes](https://quantumsoftware.tech/en/docs/nm-language/releases) ·
177
- [Türkçe sürüm notları](https://quantumsoftware.tech/tr/docs/nm-language/releases)
178
-
179
- ## License / Lisans
180
-
181
- Free personal, educational and commercial use; source-code rights are reserved.
182
- Modification or redistribution of QuantumSoftware-owned package code requires
183
- separate permission. Your N/M programs and results remain yours. See
184
- [LICENSE.txt](./LICENSE.txt) and [NOTICE.txt](./NOTICE.txt).
185
-
186
- Türkçe: Kullanım ücretsizdir; açık kaynak lisansı verilmez. Paket kodunu
187
- değiştirme ve yeniden dağıtma için ayrıca izin gerekir.
188
-
189
- ## Python/Jupyter bridge / Python/Jupyter köprüsü
190
-
191
- Version 0.5.0 adds `nm bridge` for bounded JSON `check`, `run` and
192
- `train` requests through stdin. The `python/nm_bridge.py` stdlib adapter and
193
- `python/examples/training.ipynb` preserve source, explicit caller permissions,
194
- training options and complete measurement/confidence evidence. TR/EN output is
195
- supported; saved records never self-grant replay permissions. No separate PyPI
196
- release is provided. See `python/README.md` and the repository's
189
+ ## Hamiltonian 0.4.0 / Hamiltonian 0.4.0
190
+
191
+ Version 0.4.0 includes Hamiltonian evolution and shared-shot QWC expectations.
192
+ Earlier releases do not include these additions.
193
+ Both `hamiltonianEvolution` and `measurementGrouping` require explicit permission.
194
+ Source printing and N/M IR v0.5 preserve original source, parser context, seed and
195
+ shot plans. Imports rebuild with caller permissions; stored flags never grant them.
196
+ QASM/framework export and source optimization/transpilation reject these semantics.
197
+
198
+ CLI: `--experimental-hamiltonian-evolution`, `--experimental-measurement-grouping`,
199
+ `--from-ir` for a saved v0.5 IR, `--locale en|tr` for diagnostics. MCP: the matching
200
+ `experimentalHamiltonianEvolution` / `experimentalMeasurementGrouping` booleans,
201
+ `nm_export` target `ir` and `nm_import_ir`. LSP initialization uses
202
+ `initializationOptions.nm.experimental`; VS Code uses
203
+ `nm.experimental.hamiltonianEvolution` / `nm.experimental.measurementGrouping`.
204
+ The editor offers completion/hover, closed-default diagnostics and original
205
+ source columns for generated gates in the recorded debugger.
206
+
207
+ Türkçe: 0.4.0 bu geliştirmeleri içerir; iki yeni izin varsayılan kapalıdır.
208
+ Yazdırma/IR özgün kaynağı ve ortak ölçüm bütçesini korur; içe aktarım
209
+ kaydı çalıştırma yetkisi saymaz. CLI/MCP/LSP/Monaco/VS Code aynı izinleri taşır;
210
+ tanılar ve ayarlar TR/EN destekler. TR/EN laboratuvarı sitede erişilebilirdir.
211
+ N/M dil sözleşmesi 0.2.0 ve ön izleme olgunluğu korunur.
212
+
213
+ [Release notes](https://quantumsoftware.tech/en/docs/nm-language/releases) ·
214
+ [Türkçe sürüm notları](https://quantumsoftware.tech/tr/docs/nm-language/releases)
215
+
216
+ ## License / Lisans
217
+
218
+ Free personal, educational and commercial use; source-code rights are reserved.
219
+ Modification or redistribution of QuantumSoftware-owned package code requires
220
+ separate permission. Your N/M programs and results remain yours. See
221
+ [LICENSE.txt](./LICENSE.txt) and [NOTICE.txt](./NOTICE.txt).
222
+
223
+ Türkçe: Kullanım ücretsizdir; açık kaynak lisansı verilmez. Paket kodunu
224
+ değiştirme ve yeniden dağıtma için ayrıca izin gerekir.
225
+
226
+ ## Python/Jupyter bridge / Python/Jupyter köprüsü
227
+
228
+ Version 0.5.0 adds `nm bridge` for bounded JSON `check`, `run` and
229
+ `train` requests through stdin. The `python/nm_bridge.py` stdlib adapter and
230
+ `python/examples/training.ipynb` preserve source, explicit caller permissions,
231
+ training options and complete measurement/confidence evidence. TR/EN output is
232
+ supported; saved records never self-grant replay permissions. No separate PyPI
233
+ release is provided. See `python/README.md` and the repository's
197
234
  `docs/NM_PYTHON_JUPYTER_BRIDGE_GUIDE.md`. The earlier npm 0.4.0 release does not include this bridge.
198
235
  Türkçe: 0.5.0, standart kütüphaneli yerel Python/Jupyter köprüsünü içerir;
199
236
  kayıtlar özgün kaynak, eğitim seçenekleri, bütçe ve güven kanıtını korur.
200
- Kayıt okumak çalıştırma izni vermez; ayrı bir PyPI paketi yayımlanmaz.
237
+ Kayıt okumak çalıştırma izni vermez; ayrı bir PyPI paketi yayımlanmaz.
package/bin/nm.mjs CHANGED
@@ -3,6 +3,7 @@ import { readFileSync, readdirSync, writeFileSync } from "node:fs";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { createNMProject } from "./project-create.mjs";
6
7
 
7
8
  let runNMCLI;
8
9
  let validateNMWorkspaceManifestJsonText;
@@ -107,6 +108,7 @@ if (cliArgs[0] === "bridge") {
107
108
  readWorkspaceDocuments: (entryPath) =>
108
109
  getWorkspaceIndex(entryPath).documents,
109
110
  fileExists: (filePath) => existsSync(filePath),
111
+ createProject: createNMProject,
110
112
  writeFile: (filePath, content, { overwrite }) =>
111
113
  writeFileSync(filePath, content, {
112
114
  encoding: "utf8",
@@ -0,0 +1,27 @@
1
+ import type { NMProjectPlan, NMProjectCreationResult } from "@nm-lang/core";
2
+ export type NMProjectCreationErrorCode = "NM-PROJECT-PLAN-001" | "NM-PROJECT-PATH-001" | "NM-PROJECT-LINK-001" | "NM-PROJECT-CONFLICT-001" | "NM-PROJECT-WRITE-001";
3
+ export class NMProjectCreationError extends Error {
4
+ readonly code: NMProjectCreationErrorCode;
5
+ readonly path?: string;
6
+ constructor(code: NMProjectCreationErrorCode, message: string, filePath?: string);
7
+ }
8
+ /** Minimal synchronous host contract; importing the API requires no Node type package. */
9
+ export interface NMProjectFileIdentity {
10
+ readonly dev: number | bigint;
11
+ readonly ino: number | bigint;
12
+ isDirectory(): boolean;
13
+ isSymbolicLink(): boolean;
14
+ }
15
+ export interface NMProjectFileSystem {
16
+ lstatSync(path: string): NMProjectFileIdentity;
17
+ fstatSync(descriptor: number): NMProjectFileIdentity;
18
+ readdirSync(path: string): string[];
19
+ mkdirSync(path: string): unknown;
20
+ openSync(path: string, flags: "wx"): number;
21
+ closeSync(descriptor: number): void;
22
+ writeFileSync(descriptor: number, data: Uint8Array): void;
23
+ readFileSync(path: string): { readonly length: number; equals(data: Uint8Array): boolean };
24
+ unlinkSync(path: string): void;
25
+ rmdirSync(path: string): void;
26
+ }
27
+ export function createNMProject(destination: string, plan: NMProjectPlan, options?: { fileSystem?: NMProjectFileSystem }): NMProjectCreationResult;
@@ -0,0 +1,122 @@
1
+ import * as nativeFS from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export class NMProjectCreationError extends Error {
5
+ constructor(code, message, filePath) {
6
+ super(`${code}: ${message}${filePath ? ` (${filePath})` : ""}`);
7
+ this.name = "NMProjectCreationError";
8
+ this.code = code;
9
+ this.path = filePath;
10
+ }
11
+ }
12
+ const failure = (code, message, filePath) => new NMProjectCreationError(code, message, filePath);
13
+ const unsafe = /[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/u;
14
+ const portableSegment = value => value && value !== "." && value !== ".." && !/[<>:"|?*\\/]/u.test(value) && !/[. ]$/u.test(value) && !/^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³]|conin\$|conout\$)$/iu.test(value.split(".")[0].trimEnd());
15
+ const aliasKey = value => value.normalize("NFC").toLowerCase();
16
+ const sameIdentity = (first, second) => Boolean(first && second && first.dev === second.dev && first.ino === second.ino && !second.isSymbolicLink());
17
+ function inspect(fs, filePath) {
18
+ try { return fs.lstatSync(filePath); }
19
+ catch (error) { if (error.code === "ENOENT") return undefined; throw error; }
20
+ }
21
+ function ancestors(directory) {
22
+ const items = [];
23
+ for (let current = directory; ; current = path.dirname(current)) {
24
+ items.unshift(current);
25
+ if (path.dirname(current) === current) return items;
26
+ }
27
+ }
28
+ function checkDirectories(fs, directories, identities = new Map()) {
29
+ for (const directory of directories) {
30
+ const item = inspect(fs, directory);
31
+ if (item?.isSymbolicLink()) throw failure("NM-PROJECT-LINK-001", "Symbolic links and junctions are not project destinations.", directory);
32
+ if (item && !item.isDirectory()) throw failure("NM-PROJECT-CONFLICT-001", "A project directory is occupied by a file.", directory);
33
+ if (identities.has(directory) && !sameIdentity(identities.get(directory), item)) throw failure("NM-PROJECT-CONFLICT-001", "A directory changed during project creation.", directory);
34
+ }
35
+ }
36
+ function checkAlias(fs, directory) {
37
+ const parent = path.dirname(directory);
38
+ if (!inspect(fs, parent)) return;
39
+ const name = path.basename(directory);
40
+ if (fs.readdirSync(parent).some(entry => entry !== name && aliasKey(entry) === aliasKey(name))) throw failure("NM-PROJECT-PATH-001", "Destination aliases an existing path by case or Unicode normalization.", directory);
41
+ }
42
+ function readPlan(plan, directory) {
43
+ if (!plan || plan.format !== "nm-project-plan" || plan.version !== "0.1" || plan.entry !== "src/main.nm" || !Array.isArray(plan.files) || plan.files.length !== 5) throw failure("NM-PROJECT-PLAN-001", "Expected a complete N/M project plan.");
44
+ const expected = new Set(["nm-workspace.json", "src/main.nm", "src/helper.nm", "tests/main.test.nm", "README.md"]);
45
+ const files = plan.files.map(file => {
46
+ if (!file || typeof file.path !== "string" || typeof file.content !== "string" || unsafe.test(file.path) || !file.path.split("/").every(portableSegment) || !expected.delete(file.path)) throw failure("NM-PROJECT-PLAN-001", "Project files must have unique, portable, contained paths.");
47
+ const target = path.resolve(directory, file.path);
48
+ const relative = path.relative(directory, target);
49
+ if (!relative || relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) throw failure("NM-PROJECT-PATH-001", "Project file escapes its destination.", target);
50
+ return { path: target, content: Buffer.from(file.content, "utf8") };
51
+ });
52
+ if (expected.size) throw failure("NM-PROJECT-PLAN-001", "Project plan is incomplete.");
53
+ return files;
54
+ }
55
+
56
+ /**
57
+ * Sync Node host for the pure Core plan. It never overwrites files. A complete
58
+ * preflight precedes mutation; rollback removes only the files/directories this
59
+ * call created, with matching identities and unchanged content, without recursion.
60
+ * fileSystem is an injectable Node-fs-compatible adapter for host failure tests.
61
+ */
62
+ export function createNMProject(destination, plan, { fileSystem: fs = nativeFS } = {}) {
63
+ if (typeof destination !== "string" || !destination || unsafe.test(destination) || /^[\\/]{2}[?.][\\/]/u.test(destination) || destination.replace(/\\/gu, "/").split("/").some(segment => segment === "..") || /^[a-z]:(?![\\/])/iu.test(destination)) throw failure("NM-PROJECT-PATH-001", "Expected a safe project directory.");
64
+ const directory = path.resolve(destination);
65
+ if (!directory.slice(path.parse(directory).root.length).split(path.sep).every(portableSegment)) throw failure("NM-PROJECT-PATH-001", "Project directory has an unsafe or aliased component.", directory);
66
+ const files = readPlan(plan, directory);
67
+ const directories = [...new Set([directory, ...files.map(file => path.dirname(file.path))].flatMap(ancestors))].sort((a, b) => ancestors(a).length - ancestors(b).length || a.localeCompare(b));
68
+ const identities = new Map(), createdDirectories = [], createdFiles = [];
69
+ try {
70
+ // Validate every ancestor, planned file, conflict, and normalization alias
71
+ // before the first mkdir/open, including broken links (lstat, not exists).
72
+ checkDirectories(fs, directories);
73
+ for (const item of directories) {
74
+ checkAlias(fs, item);
75
+ const stat = inspect(fs, item);
76
+ if (stat) identities.set(item, stat);
77
+ }
78
+ if (inspect(fs, directory) && fs.readdirSync(directory).length) throw failure("NM-PROJECT-CONFLICT-001", "Project destination must be empty.", directory);
79
+ for (const file of files) if (inspect(fs, file.path)) throw failure("NM-PROJECT-CONFLICT-001", "A project file already exists.", file.path);
80
+ for (const item of directories) {
81
+ checkDirectories(fs, ancestors(item), identities);
82
+ if (identities.has(item)) continue;
83
+ checkAlias(fs, item);
84
+ fs.mkdirSync(item);
85
+ const stat = fs.lstatSync(item);
86
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw failure("NM-PROJECT-LINK-001", "A created directory changed into a link.", item);
87
+ identities.set(item, stat);
88
+ createdDirectories.push({ path: item, stat });
89
+ }
90
+ for (const file of files) {
91
+ checkDirectories(fs, directories, identities);
92
+ const descriptor = fs.openSync(file.path, "wx");
93
+ try {
94
+ const stat = fs.fstatSync(descriptor);
95
+ const owned = { ...file, stat, completed: false };
96
+ createdFiles.push(owned);
97
+ fs.writeFileSync(descriptor, file.content);
98
+ owned.completed = true;
99
+ } finally { fs.closeSync(descriptor); }
100
+ }
101
+ return { directory, entryPath: path.join(directory, plan.entry), files: files.map(file => file.path) };
102
+ } catch (error) {
103
+ for (const file of createdFiles.reverse()) {
104
+ try {
105
+ checkDirectories(fs, ancestors(path.dirname(file.path)), identities);
106
+ if (!sameIdentity(file.stat, inspect(fs, file.path))) continue;
107
+ const current = fs.readFileSync(file.path);
108
+ const unchanged = file.completed ? current.equals(file.content) : current.length <= file.content.length && current.equals(file.content.subarray(0, current.length));
109
+ if (unchanged) fs.unlinkSync(file.path);
110
+ } catch { /* Preserve anything whose ownership or contents cannot be verified. */ }
111
+ }
112
+ for (const item of createdDirectories.reverse()) {
113
+ try {
114
+ checkDirectories(fs, ancestors(path.dirname(item.path)), identities);
115
+ if (sameIdentity(item.stat, inspect(fs, item.path))) fs.rmdirSync(item.path);
116
+ } catch { /* Nonempty or changed directories belong to the caller. */ }
117
+ }
118
+ if (error instanceof NMProjectCreationError) throw error;
119
+ const code = error.code === "EEXIST" || error.code === "ENOTEMPTY" ? "NM-PROJECT-CONFLICT-001" : "NM-PROJECT-WRITE-001";
120
+ throw failure(code, error instanceof Error ? error.message : "Unable to create project.", directory);
121
+ }
122
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { NM_DOCTOR_REPORT_FORMAT, NM_DOCTOR_REPORT_VERSION, createNMDoctorReport, formatNMDoctorReport, nmDoctorExpectedVersions, getNMWorkspaceModuleName, evaluateNMLanguagePreviewReadiness, NM_CLI_CONTRACT_VERSION, NM_LANGUAGE_PREVIEW_READINESS_FORMAT, NM_LANGUAGE_PREVIEW_READINESS_VERSION, NM_WORKSPACE_MAX_DOCUMENTS, NM_WORKSPACE_MAX_SOURCE_BYTES, nmCliCommandContracts, nmCliCommands, nmCliExitCodeContract, nmLanguagePreviewReadiness, nmLanguageReleaseChangelog, nmVersionManifest, nmWorkspaceDiagnosticCodes, runNMCLI, validateNMWorkspaceManifestJsonText } from "@nm-lang/core/cli";
2
- export type { NMDoctorCheck, NMDoctorComponentId, NMDoctorObservation, NMDoctorReport, NMDoctorRuntimeObservation, NMCapabilityStatus, NMCLICommand, NMCLICommandContract, NMCLIDeterminism, NMCLIExportTarget, NMCLIOptions, NMCLIOutputKind, NMCLIResult, NMLanguagePreviewReadiness, NMLanguageReleaseChange, NMVersionManifest } from "@nm-lang/core/cli";
1
+ export { NMProjectTemplateError, createNMProjectPlan, isNMProjectName, nmProjectTemplates, NM_DOCTOR_REPORT_FORMAT, NM_DOCTOR_REPORT_VERSION, createNMDoctorReport, formatNMDoctorReport, nmDoctorExpectedVersions, getNMWorkspaceModuleName, evaluateNMLanguagePreviewReadiness, NM_CLI_CONTRACT_VERSION, NM_LANGUAGE_PREVIEW_READINESS_FORMAT, NM_LANGUAGE_PREVIEW_READINESS_VERSION, NM_WORKSPACE_MAX_DOCUMENTS, NM_WORKSPACE_MAX_SOURCE_BYTES, nmCliCommandContracts, nmCliCommands, nmCliExitCodeContract, nmLanguagePreviewReadiness, nmLanguageReleaseChangelog, nmVersionManifest, nmWorkspaceDiagnosticCodes, runNMCLI, validateNMWorkspaceManifestJsonText } from "@nm-lang/core/cli";
2
+ export type { NMProjectTemplateId, NMProjectLocale, NMProjectTemplate, NMProjectPlan, NMProjectCreationResult, NMProjectTemplateErrorCode, NMDoctorCheck, NMDoctorComponentId, NMDoctorObservation, NMDoctorReport, NMDoctorRuntimeObservation, NMCapabilityStatus, NMCLICommand, NMCLICommandContract, NMCLIDeterminism, NMCLIExportTarget, NMCLIOptions, NMCLIOutputKind, NMCLIResult, NMLanguagePreviewReadiness, NMLanguageReleaseChange, NMVersionManifest } from "@nm-lang/core/cli";