naider 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/naide.js CHANGED
@@ -1,14 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { readFileSync, writeFileSync, unlinkSync, watch as fsWatch, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
4
- import { resolve, basename, extname, join, relative } from 'path';
4
+ import { resolve, basename, dirname, extname, join, relative } from 'path';
5
+ import { randomBytes } from 'crypto';
5
6
  import { compile, compileAsync } from '../src/index.js';
6
7
  import { spawn } from 'child_process';
8
+ import { createPackageInstallPlan, getDoctorReport, inspectProject, listPackageAdapters, recordPackageDependency, runPackageInstall, runProjectTests, spawnNativeTool } from '../src/project-tools.js';
7
9
 
8
10
  import { createRequire } from 'module';
9
11
  const _require = createRequire(import.meta.url);
10
12
 
11
- const NAIDE_VERSION = '2.0.0';
13
+ const NAIDE_VERSION = JSON.parse(
14
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
15
+ ).version;
12
16
 
13
17
  function crashReport(err, context = {}) {
14
18
  const info = [
@@ -38,6 +42,24 @@ function reportRetiredNx(file) {
38
42
  console.error(` Error: ${file} uses retired NAIDE-X syntax. Use standard NAIDE syntax in a .naide file.`);
39
43
  }
40
44
 
45
+ function parseNpmPackageSpec(spec) {
46
+ const match = spec.match(/^(?<name>@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._~-]*|[a-z0-9][a-z0-9._~-]*)(?:@(?<version>latest|[A-Za-z][A-Za-z0-9._-]*|[~^]?v?\d+(?:\.\d+){0,2}(?:-[A-Za-z0-9.-]+)?(?:\+[A-Za-z0-9.-]+)?))?$/);
47
+ return match ? { name: match.groups.name, version: match.groups.version } : null;
48
+ }
49
+
50
+ function spawnNpm(args, options = {}) {
51
+ const npmCliCandidates = [
52
+ process.env.npm_execpath,
53
+ join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
54
+ join(dirname(dirname(process.execPath)), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
55
+ ].filter(Boolean).map(path => resolve(path));
56
+ const npmCli = npmCliCandidates.find(path => existsSync(path));
57
+
58
+ if (npmCli) return spawn(process.execPath, [npmCli, ...args], options);
59
+ if (process.platform !== 'win32') return spawn('npm', args, options);
60
+ throw new Error('Could not locate npm-cli.js. Run this command from an npm installation or set npm_execpath.');
61
+ }
62
+
41
63
  process.on('uncaughtException', (err) => {
42
64
  crashReport(err);
43
65
  process.exit(99);
@@ -143,6 +165,65 @@ for (let i = 0; i < args.length; i++) {
143
165
  }
144
166
  }
145
167
 
168
+ if (files[0] === 'doctor') {
169
+ const directory = files[1] && !files[1].startsWith('--') ? files[1] : '.';
170
+ const report = await getDoctorReport(resolve(directory));
171
+ if (files.includes('--json')) {
172
+ console.log(JSON.stringify(report, null, 2));
173
+ } else {
174
+ console.log(`\n NAIDE doctor — ${report.project.directory}`);
175
+ console.log(` Node.js ${report.node.version}${report.node.supported ? '' : ' (unsupported; NAIDE requires >=18)'}`);
176
+ console.log(` Platform: ${report.platform}`);
177
+ console.log(` Project manifests: ${report.project.manifests.join(', ') || 'none found'}`);
178
+ for (const tool of report.tools) {
179
+ console.log(` ${tool.available ? 'OK' : '--'} ${tool.executable}${tool.version ? ` (${tool.version})` : ''}`);
180
+ }
181
+ for (const check of report.project.checks) console.log(` OK ${check}`);
182
+ for (const warning of report.project.warnings) console.log(` WARN ${warning}`);
183
+ for (const error of report.project.errors) console.error(` ERROR ${error}`);
184
+ console.log(`\n ${report.project.ok ? 'Project checks passed.' : 'Project checks found errors.'}`);
185
+ }
186
+ process.exit(report.node.supported && report.project.ok ? 0 : 1);
187
+ }
188
+
189
+ if (files[0] === 'verify') {
190
+ const directory = resolve(files[1] && !files[1].startsWith('--') ? files[1] : '.');
191
+ const report = inspectProject(directory);
192
+ if (files.includes('--json')) {
193
+ console.log(JSON.stringify(report, null, 2));
194
+ } else {
195
+ console.log(`\n NAIDE verify — ${report.directory}`);
196
+ for (const check of report.checks) console.log(` OK ${check}`);
197
+ for (const warning of report.warnings) console.log(` WARN ${warning}`);
198
+ for (const error of report.errors) console.error(` ERROR ${error}`);
199
+ console.log(`\n ${report.ok ? 'Project verification passed.' : 'Project verification failed.'}`);
200
+ }
201
+ process.exit(report.ok ? 0 : 1);
202
+ }
203
+
204
+ if (files[0] === 'test') {
205
+ const directory = files[1] && !files[1].startsWith('--') ? files[1] : '.';
206
+ let report;
207
+ try {
208
+ report = await runProjectTests(resolve(directory));
209
+ } catch (error) {
210
+ console.error(` Could not inspect test project: ${error.message}`);
211
+ process.exit(1);
212
+ }
213
+ if (report.plans.length === 0) {
214
+ console.error(' No configured native test suite found. Add a test script or a supported test project.');
215
+ process.exit(1);
216
+ }
217
+ for (const outcome of report.outcomes) {
218
+ console.log(`\n > ${outcome.label}`);
219
+ if (outcome.stdout) process.stdout.write(outcome.stdout);
220
+ if (outcome.stderr) process.stderr.write(outcome.stderr);
221
+ if (outcome.error) console.error(` ${outcome.error}`);
222
+ console.log(` ${outcome.code === 0 ? 'PASS' : 'FAIL'} ${outcome.label}`);
223
+ }
224
+ process.exit(report.ok ? 0 : 1);
225
+ }
226
+
146
227
  // ── Code Generator (~~) ──
147
228
  if (files[0] === 'gen' || files[0] === '~~') {
148
229
  const instruction = files.slice(1).join(' ');
@@ -427,7 +508,7 @@ if (flags.expand && files.length > 0) {
427
508
  if (files[0] === 'vscode') {
428
509
  const os = await import('os');
429
510
  const extSrc = new URL('../vscode-naide', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
430
- const extName = 'irxk.naide-lang-2.0.0';
511
+ const extName = 'irxk.naide-lang-2.0.1';
431
512
  const extDir = join(os.homedir(), '.vscode', 'extensions', extName);
432
513
 
433
514
  if (existsSync(extDir)) {
@@ -460,7 +541,7 @@ if (files[0] === 'vscode') {
460
541
  `);
461
542
  } catch (e) {
462
543
  console.error(` Error: ${e.message}`);
463
- console.log('\n Manual install: copy vscode-naide/ to ~/.vscode/extensions/irxk.naide-lang-2.0.0/');
544
+ console.log('\n Manual install: copy vscode-naide/ to ~/.vscode/extensions/irxk.naide-lang-2.0.1/');
464
545
  process.exit(1);
465
546
  }
466
547
  process.exit(0);
@@ -477,7 +558,7 @@ if (files[0] === 'repl' || (files.length === 0 && !flags.help)) {
477
558
  const { createInterface } = await import('readline');
478
559
  const { transpile } = await import('../src/index.js');
479
560
 
480
- console.log(`\n NAIDE REPL v1.11.0 — type NAIDE code, see JavaScript output`);
561
+ console.log(`\n NAIDE REPL v${NAIDE_VERSION} — type NAIDE code, see JavaScript output`);
481
562
  console.log(` Type .exit to quit, .eval to toggle eval mode\n`);
482
563
 
483
564
  const rl = createInterface({
@@ -612,12 +693,38 @@ if (files[0] === 'init') {
612
693
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
613
694
  const name = basename(dir === process.cwd() ? dir : dir);
614
695
 
615
- if (!existsSync(resolve(dir, 'package.json'))) {
616
- writeFileSync(resolve(dir, 'package.json'), JSON.stringify({
617
- name, version: '1.0.0', type: 'module',
618
- scripts: { start: 'naide app.naide', dev: 'naide -w app.naide', build: 'naide --emit app.naide -o dist/app.mjs' },
619
- dependencies: { naider: '^1.9.0' }
620
- }, null, 2) + '\n');
696
+ const packagePath = resolve(dir, 'package.json');
697
+ const defaultScripts = {
698
+ start: 'node -r dotenv/config ./node_modules/naider/bin/naide.js app.naide',
699
+ dev: 'node -r dotenv/config ./node_modules/naider/bin/naide.js -w app.naide',
700
+ check: 'naide check app.naide',
701
+ test: 'node --test',
702
+ build: 'naide --emit app.naide -o dist/app.mjs',
703
+ };
704
+ const projectPackage = existsSync(packagePath)
705
+ ? JSON.parse(readFileSync(packagePath, 'utf8'))
706
+ : { name, version: '1.0.0', type: 'module' };
707
+ let packageChanged = false;
708
+ projectPackage.scripts ||= {};
709
+ for (const [script, command] of Object.entries(defaultScripts)) {
710
+ if (!projectPackage.scripts[script]) {
711
+ projectPackage.scripts[script] = command;
712
+ packageChanged = true;
713
+ }
714
+ }
715
+ projectPackage.dependencies ||= {};
716
+ for (const [dependency, version] of Object.entries({
717
+ naider: `^${NAIDE_VERSION}`,
718
+ express: '^4.21.2',
719
+ dotenv: '^16.4.7',
720
+ })) {
721
+ if (!projectPackage.dependencies[dependency] && !projectPackage.devDependencies?.[dependency]) {
722
+ projectPackage.dependencies[dependency] = version;
723
+ packageChanged = true;
724
+ }
725
+ }
726
+ if (packageChanged || !existsSync(packagePath)) {
727
+ writeFileSync(packagePath, JSON.stringify(projectPackage, null, 2) + '\n');
621
728
  }
622
729
 
623
730
  if (!existsSync(resolve(dir, 'app.naide'))) {
@@ -625,32 +732,279 @@ if (files[0] === 'init') {
625
732
 
626
733
  env:
627
734
  PORT int default(3000)
735
+ JWT_SECRET str required
736
+
737
+ schema User:
738
+ id auto
739
+ name str required min(1) max(80)
740
+ email str required email max(254)
741
+ password str required min(12) max(128)
628
742
 
629
743
  schema Item:
630
744
  id auto
745
+ ownerId str required
631
746
  name str required min(1) max(100)
632
747
  done bool default(false)
633
748
 
634
749
  server app port PORT:
635
- cors "*"
636
- cookie
750
+ cors ["http://localhost:3000", "http://localhost:5173"]
751
+ limit "/api/*" 60 "1m"
752
+ limit "/api/auth/*" 10 "1m"
753
+ auth JWT_SECRET:
754
+ protect "/api/*"
755
+ public "/api/auth/register"
756
+ public "/api/auth/login"
757
+ public "/api/health"
637
758
  static "/public"
638
- crud "/api/items" Item
639
759
 
640
760
  get "/":
641
- ret.text "NAIDE server running"
761
+ ret {name: "NAIDE API", health: "/api/health"}
642
762
 
643
763
  get "/api/health":
644
- ret {status: "ok", items: ItemStore.count()}
764
+ ret {status: "ok"}
765
+
766
+ post "/api/auth/register" (req, res):
767
+ if typeof req.body.name != "string" or typeof req.body.email != "string" or typeof req.body.password != "string":
768
+ ret.status 400 {error: "name, email, and password are required"}
769
+ if req.body.name.length < 1 or req.body.name.length > 80 or req.body.email.length > 254:
770
+ ret.status 400 {error: "name or email is too long"}
771
+ if req.body.password.length < 12 or req.body.password.length > 128:
772
+ ret.status 400 {error: "password must be between 12 and 128 characters"}
773
+ str email = req.body.email.toLowerCase().trim()
774
+ if UserStore.where({email})[0]:
775
+ ret.status 409 {error: "An account with that email already exists"}
776
+ str hashed = hash(req.body.password)
777
+ user = UserStore.create({name: req.body.name.trim(), email, password: hashed})
778
+ if user.error:
779
+ ret.status 400 {errors: user.error}
780
+ token = auth.sign({id: user.id})
781
+ ret.status 201 {token, user: {id: user.id, name: user.name, email: user.email}}
782
+
783
+ post "/api/auth/login" (req, res):
784
+ if typeof req.body.email != "string" or typeof req.body.password != "string":
785
+ ret.status 401 {error: "Invalid credentials"}
786
+ if req.body.password.length > 128:
787
+ ret.status 401 {error: "Invalid credentials"}
788
+ str email = req.body.email.toLowerCase().trim()
789
+ user = UserStore.where({email})[0]
790
+ if not user:
791
+ ret.status 401 {error: "Invalid credentials"}
792
+ if not verify(req.body.password, user.password):
793
+ ret.status 401 {error: "Invalid credentials"}
794
+ token = auth.sign({id: user.id})
795
+ ret {token}
796
+
797
+ get "/api/items" (req, res):
798
+ items = ItemStore.where({ownerId: req.user.id})
799
+ ret {data: items, total: items.length}
800
+
801
+ post "/api/items" (req, res):
802
+ if typeof req.body.name != "string" or req.body.name.length < 1 or req.body.name.length > 100:
803
+ ret.status 400 {error: "name must be between 1 and 100 characters"}
804
+ item = ItemStore.create({ownerId: req.user.id, name: req.body.name.trim(), done: false})
805
+ if item.error:
806
+ ret.status 400 {errors: item.error}
807
+ ret.status 201 item
808
+
809
+ get "/api/items/:id" (req, res):
810
+ item = ItemStore.where({id: req.params.id, ownerId: req.user.id})[0]
811
+ if not item:
812
+ ret.status 404 {error: "Item not found"}
813
+ ret item
814
+
815
+ put "/api/items/:id" (req, res):
816
+ item = ItemStore.where({id: req.params.id, ownerId: req.user.id})[0]
817
+ if not item:
818
+ ret.status 404 {error: "Item not found"}
819
+ str name = item.name
820
+ bool done = item.done
821
+ if req.body.name != undefined:
822
+ if typeof req.body.name != "string" or req.body.name.length < 1 or req.body.name.length > 100:
823
+ ret.status 400 {error: "name must be between 1 and 100 characters"}
824
+ name = req.body.name.trim()
825
+ if req.body.done != undefined:
826
+ if typeof req.body.done != "boolean":
827
+ ret.status 400 {error: "done must be a boolean"}
828
+ done = req.body.done
829
+ updated = ItemStore.update(item.id, {name, done})
830
+ ret updated
831
+
832
+ del "/api/items/:id" (req, res):
833
+ item = ItemStore.where({id: req.params.id, ownerId: req.user.id})[0]
834
+ if not item:
835
+ ret.status 404 {error: "Item not found"}
836
+ bool deleted = ItemStore.delete(item.id)
837
+ ret {deleted}
838
+ `);
839
+ }
840
+
841
+ if (!existsSync(resolve(dir, '.env'))) {
842
+ writeFileSync(resolve(dir, '.env'), `JWT_SECRET=${randomBytes(32).toString('hex')}\n`, { mode: 0o600 });
843
+ }
844
+
845
+ const gitignorePath = resolve(dir, '.gitignore');
846
+ const ignoredPaths = ['.env', 'data/', 'dist/', 'node_modules/'];
847
+ const existingGitignore = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
848
+ const missingIgnoredPaths = ignoredPaths.filter(path => !existingGitignore.split(/\r?\n/).includes(path));
849
+ if (missingIgnoredPaths.length > 0) {
850
+ const separator = existingGitignore && !existingGitignore.endsWith('\n') ? '\n' : '';
851
+ writeFileSync(gitignorePath, `${existingGitignore}${separator}${missingIgnoredPaths.join('\n')}\n`);
852
+ }
853
+
854
+ if (!existsSync(resolve(dir, 'README.md'))) {
855
+ writeFileSync(resolve(dir, 'README.md'), `# ${name}
856
+
857
+ A small NAIDE API starter with registration, login, and an items API scoped to the authenticated user.
858
+
859
+ ## Start
860
+
861
+ \`\`\`sh
862
+ npm install
863
+ npm run dev
864
+ \`\`\`
865
+
866
+ The initializer creates a random \`JWT_SECRET\` in \`.env\`. Keep that file private; it is excluded by \`.gitignore\`. The development server loads it automatically.
867
+
868
+ ## API
869
+
870
+ - \`GET /api/health\` — public health check
871
+ - \`POST /api/auth/register\` — create an account (password: 12–128 characters)
872
+ - \`POST /api/auth/login\` — receive a bearer token
873
+ - \`/api/items\` — protected CRUD API; send \`Authorization: Bearer <token>\`
874
+
875
+ The default CORS allowlist is limited to local development origins. Change it deliberately when connecting a deployed frontend. The generated in-memory/file-backed starter and single-process rate limiter are for local development, not production deployment; use a production database, shared rate limiting, HTTPS, and an operational secret store before exposing it publicly.
876
+
877
+ ## Checks
878
+
879
+ \`\`\`sh
880
+ npm run check
881
+ npm test
882
+ \`\`\`
883
+ `);
884
+ }
885
+
886
+ const testDir = resolve(dir, 'test');
887
+ if (!existsSync(testDir)) mkdirSync(testDir, { recursive: true });
888
+ const starterTestPath = resolve(testDir, 'api.test.js');
889
+ if (!existsSync(starterTestPath)) {
890
+ writeFileSync(starterTestPath, `import assert from 'node:assert/strict';
891
+ import { spawn } from 'node:child_process';
892
+ import { createServer } from 'node:net';
893
+ import { setTimeout as delay } from 'node:timers/promises';
894
+ import { test } from 'node:test';
895
+
896
+ test('starter API separates each authenticated user’s items', async () => {
897
+ const portProbe = createServer();
898
+ await new Promise((resolve, reject) => {
899
+ portProbe.once('error', reject);
900
+ portProbe.listen(0, '127.0.0.1', resolve);
901
+ });
902
+ const { port } = portProbe.address();
903
+ await new Promise((resolve, reject) => portProbe.close(error => error ? reject(error) : resolve()));
904
+
905
+ const child = spawn(process.execPath, [
906
+ '-r', 'dotenv/config',
907
+ './node_modules/naider/bin/naide.js',
908
+ 'app.naide',
909
+ ], {
910
+ cwd: process.cwd(),
911
+ env: { ...process.env, PORT: String(port) },
912
+ stdio: ['ignore', 'pipe', 'pipe'],
913
+ });
914
+ let output = '';
915
+ child.stdout.setEncoding('utf8').on('data', chunk => { output += chunk; });
916
+ child.stderr.setEncoding('utf8').on('data', chunk => { output += chunk; });
917
+
918
+ try {
919
+ const baseUrl = \`http://127.0.0.1:\${port}\`;
920
+ let health;
921
+ for (let attempt = 0; attempt < 100; attempt++) {
922
+ if (child.exitCode !== null) throw new Error(\`Starter server exited early:\\n\${output}\`);
923
+ try {
924
+ health = await fetch(\`\${baseUrl}/api/health\`);
925
+ break;
926
+ } catch {
927
+ await delay(100);
928
+ }
929
+ }
930
+ assert.ok(health, \`Starter server did not become ready:\\n\${output}\`);
931
+ assert.equal(health.status, 200);
932
+ assert.deepEqual(await health.json(), { status: 'ok' });
933
+
934
+ const unauthenticated = await fetch(\`\${baseUrl}/api/items\`);
935
+ assert.equal(unauthenticated.status, 401);
936
+
937
+ const email = \`test-\${Date.now()}@example.com\`;
938
+ const registration = await fetch(\`\${baseUrl}/api/auth/register\`, {
939
+ method: 'POST',
940
+ headers: { 'Content-Type': 'application/json' },
941
+ body: JSON.stringify({
942
+ name: 'Test User',
943
+ email,
944
+ password: 'correct-horse-battery-staple',
945
+ }),
946
+ });
947
+ assert.equal(registration.status, 201, await registration.text());
948
+ const account = await registration.json();
949
+ assert.ok(account.token);
950
+ assert.equal(account.user.password, undefined);
951
+
952
+ const created = await fetch(\`\${baseUrl}/api/items\`, {
953
+ method: 'POST',
954
+ headers: {
955
+ Authorization: \`Bearer \${account.token}\`,
956
+ 'Content-Type': 'application/json',
957
+ },
958
+ body: JSON.stringify({ name: 'Private item' }),
959
+ });
960
+ assert.equal(created.status, 201);
961
+ const privateItem = await created.json();
962
+
963
+ const items = await fetch(\`\${baseUrl}/api/items\`, {
964
+ headers: { Authorization: \`Bearer \${account.token}\` },
965
+ });
966
+ assert.equal(items.status, 200);
967
+ assert.equal((await items.json()).data[0].id, privateItem.id);
968
+
969
+ const otherRegistration = await fetch(\`\${baseUrl}/api/auth/register\`, {
970
+ method: 'POST',
971
+ headers: { 'Content-Type': 'application/json' },
972
+ body: JSON.stringify({
973
+ name: 'Other User',
974
+ email: \`other-\${Date.now()}@example.com\`,
975
+ password: 'another-correct-horse-battery',
976
+ }),
977
+ });
978
+ assert.equal(otherRegistration.status, 201);
979
+ const otherAccount = await otherRegistration.json();
980
+
981
+ const otherItems = await fetch(\`\${baseUrl}/api/items\`, {
982
+ headers: { Authorization: \`Bearer \${otherAccount.token}\` },
983
+ });
984
+ assert.deepEqual((await otherItems.json()).data, []);
985
+
986
+ const hiddenItem = await fetch(\`\${baseUrl}/api/items/\${privateItem.id}\`, {
987
+ headers: { Authorization: \`Bearer \${otherAccount.token}\` },
988
+ });
989
+ assert.equal(hiddenItem.status, 404);
990
+ } finally {
991
+ child.kill();
992
+ if (child.exitCode === null) await new Promise(resolve => child.once('exit', resolve));
993
+ }
994
+ });
645
995
  `);
646
996
  }
647
997
 
648
998
  if (!existsSync(resolve(dir, 'public'))) mkdirSync(resolve(dir, 'public'), { recursive: true });
999
+ if (!existsSync(resolve(dir, 'dist'))) mkdirSync(resolve(dir, 'dist'), { recursive: true });
649
1000
 
650
1001
  console.log(`\n NAIDE project initialized!
651
1002
 
652
1003
  ${dir === process.cwd() ? '' : ` cd ${basename(dir)}\n`} npm install
1004
+ npm run check
653
1005
  npm run dev
1006
+
1007
+ A random JWT secret and a local-development API test were created.
654
1008
  `);
655
1009
  process.exit(0);
656
1010
  }
@@ -805,6 +1159,8 @@ if (files[0] === 'pkg') {
805
1159
  Usage:
806
1160
  naide pkg init Create naide.pkg.json manifest
807
1161
  naide pkg install <name> Install a NAIDE package from npm
1162
+ naide pkg add <manager> <spec> Install through a native package manager
1163
+ naide pkg managers List supported package ecosystems
808
1164
  naide pkg publish Publish current package to npm
809
1165
  naide pkg list List installed NAIDE packages
810
1166
  `);
@@ -813,6 +1169,57 @@ if (files[0] === 'pkg') {
813
1169
 
814
1170
  const pkgManifestPath = resolve('naide.pkg.json');
815
1171
 
1172
+ if (subcmd === 'managers') {
1173
+ console.log('\n Native package ecosystems:\n');
1174
+ for (const adapter of listPackageAdapters()) {
1175
+ console.log(` ${adapter.name.padEnd(10)} ${adapter.description}`);
1176
+ }
1177
+ console.log('\n Packages run in their native runtimes. Calling one from NAIDE requires an explicit bridge or adapter.\n');
1178
+ process.exit(0);
1179
+ }
1180
+
1181
+ if (subcmd === 'add') {
1182
+ const manager = files[2];
1183
+ const spec = files[3];
1184
+ if (!manager || !spec) {
1185
+ console.error(' Usage: naide pkg add <manager> <package[@version]>');
1186
+ process.exit(1);
1187
+ }
1188
+
1189
+ let plan;
1190
+ try {
1191
+ plan = createPackageInstallPlan(manager, spec);
1192
+ } catch (error) {
1193
+ console.error(` Error: ${error.message}`);
1194
+ process.exit(1);
1195
+ }
1196
+
1197
+ console.log(` Installing ${spec} via ${manager}...`);
1198
+ try {
1199
+ const code = await runPackageInstall(plan, {
1200
+ directory: process.cwd(),
1201
+ spawnCommand: (packagePlan, cwd) => packagePlan.manager === 'npm'
1202
+ ? spawnNpm(packagePlan.args, { cwd, stdio: 'inherit' })
1203
+ : spawnNativeTool(packagePlan, cwd, { stdio: 'inherit' }),
1204
+ });
1205
+ if (code === 0) {
1206
+ try {
1207
+ recordPackageDependency(process.cwd(), plan);
1208
+ } catch (error) {
1209
+ console.error(` Error: ${spec} was installed, but package metadata could not be saved: ${error.message}`);
1210
+ process.exit(1);
1211
+ }
1212
+ console.log(`\n Installed ${spec} via ${manager}.`);
1213
+ console.log(` Recorded dependency in ${manager === 'pip' ? 'naide.pkg.json and requirements.txt' : 'naide.pkg.json'}.`);
1214
+ if (plan.note) console.log(` Note: ${plan.note}`);
1215
+ }
1216
+ process.exit(code);
1217
+ } catch (error) {
1218
+ console.error(` Error: could not start ${plan.executable}: ${error.message}`);
1219
+ process.exit(1);
1220
+ }
1221
+ }
1222
+
816
1223
  if (subcmd === 'init') {
817
1224
  if (existsSync(pkgManifestPath)) {
818
1225
  console.log(' naide.pkg.json already exists.');
@@ -831,6 +1238,7 @@ if (files[0] === 'pkg') {
831
1238
  keywords: ['naide', 'naide-plugin'],
832
1239
  exports: {},
833
1240
  dependencies: {},
1241
+ languageDependencies: {},
834
1242
  };
835
1243
  writeFileSync(pkgManifestPath, JSON.stringify(manifest, null, 2) + '\n');
836
1244
  console.log(`\n Created naide.pkg.json\n`);
@@ -843,25 +1251,43 @@ if (files[0] === 'pkg') {
843
1251
  console.log(' Usage: naide pkg install <package-name>');
844
1252
  process.exit(1);
845
1253
  }
1254
+ const packageSpec = parseNpmPackageSpec(pkgName);
1255
+ if (!packageSpec) {
1256
+ console.error(' Error: expected a package name, optional scope, and optional tag or version.');
1257
+ process.exit(1);
1258
+ }
846
1259
 
847
1260
  console.log(` Installing ${pkgName}...`);
848
- const child = spawn('npm', ['install', pkgName], { stdio: 'inherit', shell: true });
1261
+ let child;
1262
+ try {
1263
+ child = spawnNpm(['install', '--', pkgName], { stdio: 'inherit' });
1264
+ } catch (error) {
1265
+ console.error(` Error: ${error.message}`);
1266
+ process.exit(1);
1267
+ }
1268
+ child.once('error', (error) => {
1269
+ console.error(` Error: failed to start npm: ${error.message}`);
1270
+ process.exit(1);
1271
+ });
849
1272
  child.on('close', (code) => {
850
1273
  if (code === 0) {
851
1274
  if (existsSync(pkgManifestPath)) {
852
1275
  try {
853
1276
  const manifest = JSON.parse(readFileSync(pkgManifestPath, 'utf-8'));
854
- const npmPkg = resolve('node_modules', pkgName, 'package.json');
1277
+ manifest.dependencies ||= {};
1278
+ const npmPkg = resolve('node_modules', packageSpec.name, 'package.json');
855
1279
  if (existsSync(npmPkg)) {
856
1280
  const ver = JSON.parse(readFileSync(npmPkg, 'utf-8')).version;
857
- manifest.dependencies[pkgName] = `^${ver}`;
1281
+ manifest.dependencies[packageSpec.name] = `^${ver}`;
858
1282
  writeFileSync(pkgManifestPath, JSON.stringify(manifest, null, 2) + '\n');
859
1283
  }
860
- } catch {}
1284
+ } catch (error) {
1285
+ console.error(` Warning: package installed, but naide.pkg.json could not be updated: ${error.message}`);
1286
+ }
861
1287
  }
862
1288
  console.log(`\n Installed ${pkgName}`);
863
1289
  }
864
- process.exit(code);
1290
+ process.exit(code ?? 1);
865
1291
  });
866
1292
  await new Promise(() => {});
867
1293
  }
@@ -887,10 +1313,20 @@ if (files[0] === 'pkg') {
887
1313
  }
888
1314
 
889
1315
  console.log(` Publishing ${manifest.name}@${manifest.version}...`);
890
- const child = spawn('npm', ['publish', '--access', 'public'], { stdio: 'inherit', shell: true });
1316
+ let child;
1317
+ try {
1318
+ child = spawnNpm(['publish', '--access', 'public'], { stdio: 'inherit' });
1319
+ } catch (error) {
1320
+ console.error(` Error: ${error.message}`);
1321
+ process.exit(1);
1322
+ }
1323
+ child.once('error', (error) => {
1324
+ console.error(` Error: failed to start npm: ${error.message}`);
1325
+ process.exit(1);
1326
+ });
891
1327
  child.on('close', (code) => {
892
1328
  if (code === 0) console.log(`\n Published ${manifest.name}@${manifest.version}`);
893
- process.exit(code);
1329
+ process.exit(code ?? 1);
894
1330
  });
895
1331
  await new Promise(() => {});
896
1332
  }
@@ -902,13 +1338,18 @@ if (files[0] === 'pkg') {
902
1338
  }
903
1339
  const manifest = JSON.parse(readFileSync(pkgManifestPath, 'utf-8'));
904
1340
  const deps = Object.entries(manifest.dependencies || {});
905
- if (deps.length === 0) {
1341
+ const languageDeps = Object.entries(manifest.languageDependencies || {})
1342
+ .flatMap(([manager, packages]) => Object.entries(packages).map(([name, spec]) => [name, spec, manager]));
1343
+ if (deps.length === 0 && languageDeps.length === 0) {
906
1344
  console.log(' No NAIDE packages installed.');
907
1345
  } else {
908
1346
  console.log('\n NAIDE packages:\n');
909
1347
  for (const [name, ver] of deps) {
910
1348
  console.log(` ${name} ${ver}`);
911
1349
  }
1350
+ for (const [name, spec, manager] of languageDeps) {
1351
+ console.log(` ${name} ${spec} (${manager})`);
1352
+ }
912
1353
  console.log('');
913
1354
  }
914
1355
  process.exit(0);
@@ -986,9 +1427,14 @@ if (flags.help) {
986
1427
  naide lsp Start language server (LSP)
987
1428
  naide vscode Install VS Code extension
988
1429
  naide deploy [dir] Generate Dockerfile for deployment
1430
+ naide doctor [dir] Diagnose local tools and project setup
1431
+ naide verify [dir] Check project manifests and setup
1432
+ naide test [dir] Run detected native project test suites
989
1433
  naide fmt <files...> Format NAIDE files
990
1434
  naide pkg init Create naide.pkg.json manifest
991
1435
  naide pkg install <name> Install a NAIDE package
1436
+ naide pkg add <manager> <spec> Install with a native package manager
1437
+ naide pkg managers List supported package ecosystems
992
1438
  naide pkg publish Publish package to npm
993
1439
  naide pkg list List NAIDE dependencies
994
1440
  naide --emit <file> Output generated JavaScript
@@ -1163,7 +1609,7 @@ for (const file of files) {
1163
1609
  const tempFile = resolve(`.naide_tmp_${basename(file, ext)}.mjs`);
1164
1610
  writeFileSync(tempFile, outputCode, 'utf-8');
1165
1611
  console.log(`[NAIDE] Running with Bun — ${file}`);
1166
- const bunChild = spawn('bun', ['run', tempFile], { stdio: 'inherit', shell: true });
1612
+ const bunChild = spawn('bun', ['run', tempFile], { stdio: 'inherit' });
1167
1613
  bunChild.on('close', (code) => {
1168
1614
  try { unlinkSync(tempFile); } catch {}
1169
1615
  process.exit(code || 0);