naider 1.24.5 → 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 = '1.24.5';
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 = [
@@ -34,6 +38,28 @@ function crashReport(err, context = {}) {
34
38
  console.error(` └──────────────────────────────────────────────────\n`);
35
39
  }
36
40
 
41
+ function reportRetiredNx(file) {
42
+ console.error(` Error: ${file} uses retired NAIDE-X syntax. Use standard NAIDE syntax in a .naide file.`);
43
+ }
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
+
37
63
  process.on('uncaughtException', (err) => {
38
64
  crashReport(err);
39
65
  process.exit(99);
@@ -107,8 +133,6 @@ const flags = {
107
133
  tokens: false,
108
134
  output: null,
109
135
  help: false,
110
- mode: null,
111
- mid: false,
112
136
  watch: false,
113
137
  debug: false,
114
138
  check: false,
@@ -129,8 +153,6 @@ for (let i = 0; i < args.length; i++) {
129
153
  case '--tokens': flags.tokens = true; flags.run = false; break;
130
154
  case '--output': case '-o': flags.output = args[++i]; break;
131
155
  case '--help': case '-h': flags.help = true; break;
132
- case '--x': case '-x': flags.mode = 'x'; break;
133
- case '--mid': flags.mid = true; flags.run = false; break;
134
156
  case '--watch': case '-w': flags.watch = true; break;
135
157
  case '--debug': case '-d': flags.debug = true; break;
136
158
  case '--check': flags.check = true; flags.run = false; break;
@@ -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(' ');
@@ -221,11 +302,11 @@ if (files[0] === 'gen' || files[0] === '~~') {
221
302
 
222
303
  const { generate, generateProject, updateProject, writeToFile } = await import('../src/gen.js');
223
304
 
224
- // Write mode: inject into existing .naide/.nx file
225
- const targetFile = instruction.match(/\S+\.(?:naide|nx)$/)?.[0];
305
+ // Write mode: inject into existing .naide file
306
+ const targetFile = instruction.match(/\S+\.naide$/)?.[0];
226
307
  if (targetFile || flags.write) {
227
308
  const file = flags.write || targetFile;
228
- const cleanInstruction = targetFile ? instruction.replace(/\s+\S+\.(?:naide|nx)$/, '') : instruction;
309
+ const cleanInstruction = targetFile ? instruction.replace(/\s+\S+\.naide$/, '') : instruction;
229
310
  const mode = flags.replace ? 'replace' : 'append';
230
311
  const result = writeToFile(resolve(file), cleanInstruction, { mode });
231
312
  const a = result.analysis;
@@ -271,8 +352,11 @@ if (files[0] === 'gen' || files[0] === '~~') {
271
352
  }
272
353
 
273
354
  const isGame = result.analysis && result.analysis.gameType;
355
+ const isApp = result.analysis && result.analysis.appType;
274
356
  if (isGame && !flags.output && !isUpdate) {
275
357
  dir = result.analysis.gameType + '-game';
358
+ } else if (isApp && !flags.output && !isUpdate) {
359
+ dir = result.analysis.appType + '-app';
276
360
  } else {
277
361
  const primaryName = (result.entities[0] || 'app').toLowerCase().replace(/[^a-z0-9]/g, '');
278
362
  if (!flags.output && !isUpdate) dir = primaryName + '-app';
@@ -424,7 +508,7 @@ if (flags.expand && files.length > 0) {
424
508
  if (files[0] === 'vscode') {
425
509
  const os = await import('os');
426
510
  const extSrc = new URL('../vscode-naide', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
427
- const extName = 'irxk.naide-lang-1.1.0';
511
+ const extName = 'irxk.naide-lang-2.0.1';
428
512
  const extDir = join(os.homedir(), '.vscode', 'extensions', extName);
429
513
 
430
514
  if (existsSync(extDir)) {
@@ -457,7 +541,7 @@ if (files[0] === 'vscode') {
457
541
  `);
458
542
  } catch (e) {
459
543
  console.error(` Error: ${e.message}`);
460
- console.log('\n Manual install: copy vscode-naide/ to ~/.vscode/extensions/irxk.naide-lang-1.1.0/');
544
+ console.log('\n Manual install: copy vscode-naide/ to ~/.vscode/extensions/irxk.naide-lang-2.0.1/');
461
545
  process.exit(1);
462
546
  }
463
547
  process.exit(0);
@@ -474,7 +558,7 @@ if (files[0] === 'repl' || (files.length === 0 && !flags.help)) {
474
558
  const { createInterface } = await import('readline');
475
559
  const { transpile } = await import('../src/index.js');
476
560
 
477
- 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`);
478
562
  console.log(` Type .exit to quit, .eval to toggle eval mode\n`);
479
563
 
480
564
  const rl = createInterface({
@@ -609,12 +693,38 @@ if (files[0] === 'init') {
609
693
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
610
694
  const name = basename(dir === process.cwd() ? dir : dir);
611
695
 
612
- if (!existsSync(resolve(dir, 'package.json'))) {
613
- writeFileSync(resolve(dir, 'package.json'), JSON.stringify({
614
- name, version: '1.0.0', type: 'module',
615
- scripts: { start: 'naide app.naide', dev: 'naide -w app.naide', build: 'naide --emit app.naide -o dist/app.mjs' },
616
- dependencies: { naider: '^1.9.0' }
617
- }, 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');
618
728
  }
619
729
 
620
730
  if (!existsSync(resolve(dir, 'app.naide'))) {
@@ -622,32 +732,279 @@ if (files[0] === 'init') {
622
732
 
623
733
  env:
624
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)
625
742
 
626
743
  schema Item:
627
744
  id auto
745
+ ownerId str required
628
746
  name str required min(1) max(100)
629
747
  done bool default(false)
630
748
 
631
749
  server app port PORT:
632
- cors "*"
633
- 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"
634
758
  static "/public"
635
- crud "/api/items" Item
636
759
 
637
760
  get "/":
638
- ret.text "NAIDE server running"
761
+ ret {name: "NAIDE API", health: "/api/health"}
639
762
 
640
763
  get "/api/health":
641
- 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
+ });
642
995
  `);
643
996
  }
644
997
 
645
998
  if (!existsSync(resolve(dir, 'public'))) mkdirSync(resolve(dir, 'public'), { recursive: true });
999
+ if (!existsSync(resolve(dir, 'dist'))) mkdirSync(resolve(dir, 'dist'), { recursive: true });
646
1000
 
647
1001
  console.log(`\n NAIDE project initialized!
648
1002
 
649
1003
  ${dir === process.cwd() ? '' : ` cd ${basename(dir)}\n`} npm install
1004
+ npm run check
650
1005
  npm run dev
1006
+
1007
+ A random JWT secret and a local-development API test were created.
651
1008
  `);
652
1009
  process.exit(0);
653
1010
  }
@@ -663,31 +1020,34 @@ if (files[0] === 'build') {
663
1020
  if (statSync(full).isDirectory()) {
664
1021
  if (entry === 'node_modules' || entry === '.git' || entry === 'dist') continue;
665
1022
  found.push(...walkDir(full));
666
- } else if (entry.endsWith('.naide') || entry.endsWith('.nx')) {
1023
+ } else if (entry.endsWith('.naide')) {
667
1024
  found.push(full);
1025
+ } else if (entry.endsWith('.nx')) {
1026
+ retiredFiles.push(full);
668
1027
  }
669
1028
  }
670
1029
  return found;
671
1030
  }
672
1031
 
1032
+ const retiredFiles = [];
673
1033
  const sourceFiles = walkDir(dir);
1034
+ for (const file of retiredFiles) reportRetiredNx(relative(dir, file));
674
1035
  if (sourceFiles.length === 0) {
675
- console.log(' No .naide or .nx files found.');
676
- process.exit(0);
1036
+ console.log(' No .naide files found.');
1037
+ process.exit(retiredFiles.length > 0 ? 1 : 0);
677
1038
  }
678
1039
 
679
1040
  console.log(`\n NAIDE build — ${sourceFiles.length} file(s)\n`);
680
- let errors = 0;
1041
+ let errors = retiredFiles.length;
681
1042
  const schemaMap = {};
682
1043
  const routeMap = {};
683
1044
 
684
1045
  for (const srcFile of sourceFiles) {
685
1046
  const rel = relative(dir, srcFile);
686
- const mode = srcFile.endsWith('.nx') ? 'x' : 'naide';
687
1047
  try {
688
1048
  const source = readFileSync(srcFile, 'utf-8');
689
- const { js, ast } = compile(source, { mode });
690
- const outName = rel.replace(/\.(naide|nx)$/, '.mjs');
1049
+ const { js, ast } = compile(source);
1050
+ const outName = rel.replace(/\.naide$/, '.mjs');
691
1051
  const outPath = outDir ? join(outDir, outName) : join(dir, outName);
692
1052
  const outDirPath = resolve(outPath, '..');
693
1053
  if (!existsSync(outDirPath)) mkdirSync(outDirPath, { recursive: true });
@@ -742,81 +1102,26 @@ if (files[0] === 'build') {
742
1102
  process.exit(errors > 0 ? 1 : 0);
743
1103
  }
744
1104
 
745
- // ── Convert (NX ↔ NAIDE) ──
746
- if (files[0] === 'convert') {
747
- const targets = files.slice(1);
748
- if (targets.length === 0) {
749
- console.log(' Usage: naide convert <file.nx|file.naide>');
750
- console.log(' .nx → .naide Expand NX shorthand to readable NAIDE');
751
- console.log(' .naide → .nx Compress NAIDE to AI-optimized NX');
752
- process.exit(0);
753
- }
754
-
755
- for (const file of targets) {
756
- const filePath = resolve(file);
757
- const ext = extname(file);
758
- try {
759
- const source = readFileSync(filePath, 'utf-8');
760
-
761
- if (ext === '.nx') {
762
- const { preprocess } = await import('../src/preprocess.js');
763
- const naide = preprocess(source);
764
- const outPath = filePath.replace(/\.nx$/, '.naide');
765
- writeFileSync(outPath, naide);
766
- console.log(` ${file} → ${basename(outPath)}`);
767
- } else if (ext === '.naide') {
768
- const lines = source.split('\n');
769
- const nxLines = [];
770
- for (const line of lines) {
771
- let nx = line;
772
- nx = nx.replace(/^(\s*)#\s?(.*)/, '$1-- $2');
773
- nx = nx.replace(/^(\s*)fn\.async\s+/, '$1~f ');
774
- nx = nx.replace(/^(\s*)fn\s+/, '$1f ');
775
- nx = nx.replace(/^(\s*)use\s+\{(.+?)\}\s+from\s+/, '$1<{$2}');
776
- nx = nx.replace(/^(\s*)use\s+/, '$1<');
777
- nx = nx.replace(/\bret\b/, '>');
778
- nx = nx.replace(/\beach\s+(\w+)\s+in\s+/, '@$1<');
779
- nx = nx.replace(/\bawait\s+/, '~');
780
- nx = nx.replace(/\bstr\b/g, 's');
781
- nx = nx.replace(/\bint\b/g, 'i');
782
- nx = nx.replace(/\bnum\b/g, 'n');
783
- nx = nx.replace(/\bbool\b/g, 'b');
784
- nx = nx.replace(/\blist\b/g, 'l');
785
- nx = nx.replace(/\bmap\b/g, 'm');
786
- nx = nx.replace(/\bany\b/g, 'a');
787
- nx = nx.replace(/\bconsole\.log\b/, 'log');
788
- nxLines.push(nx);
789
- }
790
- const outPath = filePath.replace(/\.naide$/, '.nx');
791
- writeFileSync(outPath, nxLines.join('\n'));
792
- console.log(` ${file} → ${basename(outPath)}`);
793
- } else {
794
- console.error(` Unsupported: ${file} (use .naide or .nx)`);
795
- }
796
- } catch (e) {
797
- console.error(` Error: ${file} — ${e.message}`);
798
- }
799
- }
800
- process.exit(0);
801
- }
802
-
803
1105
  // ── Type Check ──
804
1106
  if (files[0] === 'check') {
805
1107
  const targets = files.slice(1);
806
1108
  if (targets.length === 0) {
807
- console.log(' Usage: naide check <file.naide|file.nx> [files...]');
1109
+ console.log(' Usage: naide check <file.naide> [files...]');
808
1110
  process.exit(0);
809
1111
  }
810
1112
 
811
1113
  let totalErrors = 0;
812
1114
  let totalWarnings = 0;
813
1115
  for (const file of targets) {
1116
+ if (extname(file) === '.nx') {
1117
+ reportRetiredNx(file);
1118
+ totalErrors++;
1119
+ continue;
1120
+ }
814
1121
  const filePath = resolve(file);
815
1122
  try {
816
1123
  const source = readFileSync(filePath, 'utf-8');
817
- const ext = extname(file);
818
- const mode = ext === '.nx' ? 'x' : 'naide';
819
- const result = compile(source, { mode, typeCheck: true });
1124
+ const result = compile(source, { typeCheck: true });
820
1125
  const { typeErrors } = result;
821
1126
 
822
1127
  if (typeErrors) {
@@ -854,6 +1159,8 @@ if (files[0] === 'pkg') {
854
1159
  Usage:
855
1160
  naide pkg init Create naide.pkg.json manifest
856
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
857
1164
  naide pkg publish Publish current package to npm
858
1165
  naide pkg list List installed NAIDE packages
859
1166
  `);
@@ -862,6 +1169,57 @@ if (files[0] === 'pkg') {
862
1169
 
863
1170
  const pkgManifestPath = resolve('naide.pkg.json');
864
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
+
865
1223
  if (subcmd === 'init') {
866
1224
  if (existsSync(pkgManifestPath)) {
867
1225
  console.log(' naide.pkg.json already exists.');
@@ -880,6 +1238,7 @@ if (files[0] === 'pkg') {
880
1238
  keywords: ['naide', 'naide-plugin'],
881
1239
  exports: {},
882
1240
  dependencies: {},
1241
+ languageDependencies: {},
883
1242
  };
884
1243
  writeFileSync(pkgManifestPath, JSON.stringify(manifest, null, 2) + '\n');
885
1244
  console.log(`\n Created naide.pkg.json\n`);
@@ -892,25 +1251,43 @@ if (files[0] === 'pkg') {
892
1251
  console.log(' Usage: naide pkg install <package-name>');
893
1252
  process.exit(1);
894
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
+ }
895
1259
 
896
1260
  console.log(` Installing ${pkgName}...`);
897
- 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
+ });
898
1272
  child.on('close', (code) => {
899
1273
  if (code === 0) {
900
1274
  if (existsSync(pkgManifestPath)) {
901
1275
  try {
902
1276
  const manifest = JSON.parse(readFileSync(pkgManifestPath, 'utf-8'));
903
- const npmPkg = resolve('node_modules', pkgName, 'package.json');
1277
+ manifest.dependencies ||= {};
1278
+ const npmPkg = resolve('node_modules', packageSpec.name, 'package.json');
904
1279
  if (existsSync(npmPkg)) {
905
1280
  const ver = JSON.parse(readFileSync(npmPkg, 'utf-8')).version;
906
- manifest.dependencies[pkgName] = `^${ver}`;
1281
+ manifest.dependencies[packageSpec.name] = `^${ver}`;
907
1282
  writeFileSync(pkgManifestPath, JSON.stringify(manifest, null, 2) + '\n');
908
1283
  }
909
- } catch {}
1284
+ } catch (error) {
1285
+ console.error(` Warning: package installed, but naide.pkg.json could not be updated: ${error.message}`);
1286
+ }
910
1287
  }
911
1288
  console.log(`\n Installed ${pkgName}`);
912
1289
  }
913
- process.exit(code);
1290
+ process.exit(code ?? 1);
914
1291
  });
915
1292
  await new Promise(() => {});
916
1293
  }
@@ -931,15 +1308,25 @@ if (files[0] === 'pkg') {
931
1308
  type: 'module',
932
1309
  main: manifest.main,
933
1310
  keywords: manifest.keywords,
934
- files: ['*.naide', '*.nx', 'src/', 'naide.pkg.json'],
1311
+ files: ['*.naide', 'src/', 'naide.pkg.json'],
935
1312
  }, null, 2) + '\n');
936
1313
  }
937
1314
 
938
1315
  console.log(` Publishing ${manifest.name}@${manifest.version}...`);
939
- 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
+ });
940
1327
  child.on('close', (code) => {
941
1328
  if (code === 0) console.log(`\n Published ${manifest.name}@${manifest.version}`);
942
- process.exit(code);
1329
+ process.exit(code ?? 1);
943
1330
  });
944
1331
  await new Promise(() => {});
945
1332
  }
@@ -951,13 +1338,18 @@ if (files[0] === 'pkg') {
951
1338
  }
952
1339
  const manifest = JSON.parse(readFileSync(pkgManifestPath, 'utf-8'));
953
1340
  const deps = Object.entries(manifest.dependencies || {});
954
- 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) {
955
1344
  console.log(' No NAIDE packages installed.');
956
1345
  } else {
957
1346
  console.log('\n NAIDE packages:\n');
958
1347
  for (const [name, ver] of deps) {
959
1348
  console.log(` ${name} ${ver}`);
960
1349
  }
1350
+ for (const [name, spec, manager] of languageDeps) {
1351
+ console.log(` ${name} ${spec} (${manager})`);
1352
+ }
961
1353
  console.log('');
962
1354
  }
963
1355
  process.exit(0);
@@ -985,7 +1377,6 @@ CMD ["node", "dist/app.mjs"]
985
1377
  const dockerignoreContent = `node_modules
986
1378
  .git
987
1379
  *.naide
988
- *.nx
989
1380
  .naide_tmp_*
990
1381
  dist
991
1382
  `;
@@ -1022,12 +1413,11 @@ dist
1022
1413
  if (flags.help) {
1023
1414
  console.log(`
1024
1415
  NAIDE - Node AI Development Environment
1025
- A language designed for AI-speed code generation
1416
+ One readable syntax for people and tools
1026
1417
 
1027
1418
  Usage:
1028
1419
  naide Start interactive REPL
1029
1420
  naide <file.naide> Run a NAIDE file
1030
- naide <file.nx> Run a NAIDE-X file (auto-detected)
1031
1421
  naide ~~ "instruction" Generate NAIDE code from instruction (no AI)
1032
1422
  naide gen "instruction" Same as ~~ (alias)
1033
1423
  naide init [dir] Create a new NAIDE project
@@ -1037,15 +1427,17 @@ if (flags.help) {
1037
1427
  naide lsp Start language server (LSP)
1038
1428
  naide vscode Install VS Code extension
1039
1429
  naide deploy [dir] Generate Dockerfile for deployment
1040
- naide convert <files...> Convert between .naide and .nx formats
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
1041
1433
  naide fmt <files...> Format NAIDE files
1042
1434
  naide pkg init Create naide.pkg.json manifest
1043
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
1044
1438
  naide pkg publish Publish package to npm
1045
1439
  naide pkg list List NAIDE dependencies
1046
1440
  naide --emit <file> Output generated JavaScript
1047
- naide --mid <file.nx> Output intermediate NAIDE v1 (debug)
1048
- naide -x <file.naide> Force NAIDE-X mode
1049
1441
  naide -w <file.naide> Watch mode (auto-restart on changes)
1050
1442
  naide -d <file> Debug mode (Node.js inspector)
1051
1443
 
@@ -1072,19 +1464,13 @@ if (flags.help) {
1072
1464
  php PHP
1073
1465
  ruby Ruby (Sinatra)
1074
1466
 
1075
- Modes:
1076
- .naide Standard NAIDE (~40% fewer tokens than JS)
1077
- .nx NAIDE-X extreme (~80% fewer tokens than JS)
1078
-
1079
1467
  Options:
1080
1468
  -e, --emit Print generated JavaScript
1081
1469
  -o, --output Write generated JavaScript to file
1082
- -x Force NAIDE-X mode
1083
1470
  -w, --watch Watch mode: restart on file changes
1084
1471
  -d, --debug Start with Node.js debugger (--inspect-brk)
1085
1472
  -t, --target Compile target: node (default), bun, python/py
1086
1473
  --check Type-check files without running
1087
- --mid Show intermediate NAIDE v1 (X mode only)
1088
1474
  --expand Show ~~ directive expansion (debug)
1089
1475
  --ast Print AST
1090
1476
  --tokens Print tokens
@@ -1094,6 +1480,7 @@ if (flags.help) {
1094
1480
  Intent server api rest bot cli page test database ai crud auth ws mail graphql game
1095
1481
  Composite todo blog chat shop fullstack board
1096
1482
  Game tapping quiz memory snake typing reaction breakout dodge color pong
1483
+ App calculator timer counter pomodoro clock drawing notes
1097
1484
  Platform discord slack telegram line
1098
1485
  JP Intent サーバー 認証 ログイン 会員 CRUD 管理 データベース ボット テスト ページ ゲーム
1099
1486
  JP Entity ユーザー 商品 記事 タスク 注文 コメント イベント 問い合わせ 掲示板 決済
@@ -1115,15 +1502,13 @@ for (const file of files) {
1115
1502
  }
1116
1503
 
1117
1504
  const ext = extname(file);
1118
- const mode = flags.mode || (ext === '.nx' ? 'x' : 'naide');
1505
+ if (ext === '.nx') {
1506
+ reportRetiredNx(file);
1507
+ process.exitCode = 1;
1508
+ continue;
1509
+ }
1119
1510
 
1120
1511
  try {
1121
- if (flags.mid && mode === 'x') {
1122
- const { preprocess } = await import('../src/preprocess.js');
1123
- console.log(preprocess(source));
1124
- continue;
1125
- }
1126
-
1127
1512
  const runtimePath = flags.emit || flags.output ? 'naider/runtime' : runtimeUrl;
1128
1513
 
1129
1514
  if (flags.watch) {
@@ -1133,7 +1518,7 @@ for (const file of files) {
1133
1518
  function start() {
1134
1519
  try {
1135
1520
  source = readFileSync(filePath, 'utf-8');
1136
- const result = compile(source, { mode, runtimePath });
1521
+ const result = compile(source, { runtimePath });
1137
1522
  writeFileSync(tempFile, result.js, 'utf-8');
1138
1523
  child = spawn(process.execPath, [tempFile], { stdio: 'inherit' });
1139
1524
  child.on('error', (err) => console.error(`[NAIDE] Process error: ${err.message}`));
@@ -1176,8 +1561,8 @@ for (const file of files) {
1176
1561
 
1177
1562
  const useAsync = flags.target !== 'node';
1178
1563
  const result = useAsync
1179
- ? await compileAsync(source, { mode, runtimePath, sourceFile: file, typeCheck: flags.check, target: flags.target })
1180
- : compile(source, { mode, runtimePath, sourceFile: file, typeCheck: flags.check });
1564
+ ? await compileAsync(source, { runtimePath, sourceFile: file, typeCheck: flags.check, target: flags.target })
1565
+ : compile(source, { runtimePath, sourceFile: file, typeCheck: flags.check });
1181
1566
 
1182
1567
  if (flags.check) {
1183
1568
  const { typeErrors } = result;
@@ -1224,7 +1609,7 @@ for (const file of files) {
1224
1609
  const tempFile = resolve(`.naide_tmp_${basename(file, ext)}.mjs`);
1225
1610
  writeFileSync(tempFile, outputCode, 'utf-8');
1226
1611
  console.log(`[NAIDE] Running with Bun — ${file}`);
1227
- const bunChild = spawn('bun', ['run', tempFile], { stdio: 'inherit', shell: true });
1612
+ const bunChild = spawn('bun', ['run', tempFile], { stdio: 'inherit' });
1228
1613
  bunChild.on('close', (code) => {
1229
1614
  try { unlinkSync(tempFile); } catch {}
1230
1615
  process.exit(code || 0);