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/README.md +105 -29
- package/bin/naide.js +471 -25
- package/lsp/server.js +210 -100
- package/mcp/server.js +246 -21
- package/package.json +7 -2
- package/src/generator-c.js +34 -14
- package/src/generator-go.js +26 -0
- package/src/project-tools.js +785 -0
- package/src/typechecker.js +87 -5
- package/vscode-naide/README.md +1 -1
- package/vscode-naide/package-lock.json +2 -2
- package/vscode-naide/package.json +1 -1
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 =
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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
|
|
761
|
+
ret {name: "NAIDE API", health: "/api/health"}
|
|
642
762
|
|
|
643
763
|
get "/api/health":
|
|
644
|
-
ret {status: "ok"
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
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);
|