@nostale/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/assets/docker-compose.server.yml +70 -0
- package/dist/commands/config/generation.js +23 -0
- package/dist/commands/config/providers.js +138 -0
- package/dist/commands/config/sources.js +145 -0
- package/dist/commands/config/templates.js +117 -0
- package/dist/commands/config/tokens.js +83 -0
- package/dist/commands/config.js +43 -0
- package/dist/commands/doctor.js +98 -0
- package/dist/commands/setup.js +5 -0
- package/dist/commands/start.js +23 -0
- package/dist/commands/status.js +17 -0
- package/dist/commands/stop.js +16 -0
- package/dist/index.js +23 -0
- package/dist/lib/api-config.js +21 -0
- package/dist/lib/env-writer.js +41 -0
- package/dist/lib/http.js +117 -0
- package/dist/lib/install-root.js +75 -0
- package/dist/lib/paths.js +12 -0
- package/dist/lib/ports.js +19 -0
- package/dist/lib/process-manager.js +151 -0
- package/dist/lib/repo-root.js +34 -0
- package/dist/lib/wizard.js +267 -0
- package/dist/tui/prompts.js +54 -0
- package/package.json +69 -0
- package/services/mcp-server/index.cjs +48326 -0
- package/services/migrations/001_init_rag_v1.sql +128 -0
- package/services/migrations/002_doc_lifecycle.sql +96 -0
- package/services/migrations/004_mcp_tokens.sql +11 -0
- package/services/migrations/005_settings.sql +28 -0
- package/services/migrations/006_document_sources.sql +14 -0
- package/services/migrations/007_generation_settings.sql +9 -0
- package/services/migrations/008_llm_provider_configs_unique.sql +5 -0
- package/services/wasm/tree-sitter-c.wasm +0 -0
- package/services/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/services/wasm/tree-sitter-cpp.wasm +0 -0
- package/services/wasm/tree-sitter-dart.wasm +0 -0
- package/services/wasm/tree-sitter-go.wasm +0 -0
- package/services/wasm/tree-sitter-java.wasm +0 -0
- package/services/wasm/tree-sitter-javascript.wasm +0 -0
- package/services/wasm/tree-sitter-kotlin.wasm +0 -0
- package/services/wasm/tree-sitter-php.wasm +0 -0
- package/services/wasm/tree-sitter-python.wasm +0 -0
- package/services/wasm/tree-sitter-rust.wasm +0 -0
- package/services/wasm/tree-sitter-tsx.wasm +0 -0
- package/services/wasm/tree-sitter-typescript.wasm +0 -0
- package/services/wasm/tree-sitter.wasm +0 -0
- package/services/wiki-api/index.cjs +82370 -0
- package/services/wiki-web/.output/nitro.json +15 -0
- package/services/wiki-web/.output/public/_nuxt/68caad34b1e80fa12b3a.4Cpu5VkB.woff +0 -0
- package/services/wiki-web/.output/public/_nuxt/6b042d50787d4a81d659.D7JGeKLp.svg +1835 -0
- package/services/wiki-web/.output/public/_nuxt/9065bfe481020b04b68a.CMveUrS1.woff2 +0 -0
- package/services/wiki-web/.output/public/_nuxt/B5j-AdCY.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/BAgZ2ZFT.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/BcxnhBGC.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/BeIDAJKt.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/BebqDWPm.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/CFx_Wb7e.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/CSEkmdhK.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/CXYMfke_.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/Cap3hpB7.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/Ckd7UvSj.js +3 -0
- package/services/wiki-web/.output/public/_nuxt/D0DJQL09.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/DRDlUXmk.js +66 -0
- package/services/wiki-web/.output/public/_nuxt/DW42QLqA.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/DdDtdYim.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/Dm1y7TW1.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/DptiLPkM.js +4 -0
- package/services/wiki-web/.output/public/_nuxt/QFl-TMXl.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/_id_.JvKV2jiN.css +1 -0
- package/services/wiki-web/.output/public/_nuxt/a9c9056a643325579e2a.CCNAgZDa.ttf +0 -0
- package/services/wiki-web/.output/public/_nuxt/bojElwT6.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/builds/latest.json +1 -0
- package/services/wiki-web/.output/public/_nuxt/builds/meta/f68e5546-2919-478b-b4ad-702826e8383c.json +1 -0
- package/services/wiki-web/.output/public/_nuxt/d495aba62101f856c80f.DJ4Lg1TH.eot +0 -0
- package/services/wiki-web/.output/public/_nuxt/entry.isoIPAS3.css +1 -0
- package/services/wiki-web/.output/public/_nuxt/f8QtLg7g.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/nhMvlSQm.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/pFBugkEL.js +1 -0
- package/services/wiki-web/.output/public/_nuxt/plus-jakarta-sans-latin-ext-wght-normal.DmpS2jIq.woff2 +0 -0
- package/services/wiki-web/.output/public/_nuxt/plus-jakarta-sans-latin-wght-normal.eXO_dkmS.woff2 +0 -0
- package/services/wiki-web/.output/public/_nuxt/plus-jakarta-sans-vietnamese-wght-normal.qRpaaN48.woff2 +0 -0
- package/services/wiki-web/.output/public/favicon.svg +6 -0
- package/services/wiki-web/.output/public/robots.txt +2 -0
- package/services/wiki-web/.output/server/chunks/_/error-500.mjs +19 -0
- package/services/wiki-web/.output/server/chunks/_/error-500.mjs.map +1 -0
- package/services/wiki-web/.output/server/chunks/build/client.precomputed.mjs +4 -0
- package/services/wiki-web/.output/server/chunks/build/client.precomputed.mjs.map +1 -0
- package/services/wiki-web/.output/server/chunks/nitro/nitro.mjs +5352 -0
- package/services/wiki-web/.output/server/chunks/nitro/nitro.mjs.map +1 -0
- package/services/wiki-web/.output/server/chunks/routes/api-proxy/_...path_.mjs +34 -0
- package/services/wiki-web/.output/server/chunks/routes/api-proxy/_...path_.mjs.map +1 -0
- package/services/wiki-web/.output/server/chunks/routes/renderer.mjs +369 -0
- package/services/wiki-web/.output/server/chunks/routes/renderer.mjs.map +1 -0
- package/services/wiki-web/.output/server/chunks/virtual/_virtual_spa-template.mjs +4 -0
- package/services/wiki-web/.output/server/chunks/virtual/_virtual_spa-template.mjs.map +1 -0
- package/services/wiki-web/.output/server/index.mjs +10 -0
- package/services/wiki-web/.output/server/index.mjs.map +1 -0
- package/services/wiki-web/.output/server/node_modules/@babel/parser/lib/index.js +14599 -0
- package/services/wiki-web/.output/server/node_modules/@babel/parser/package.json +50 -0
- package/services/wiki-web/.output/server/node_modules/@vue/compiler-core/dist/compiler-core.cjs.prod.js +6759 -0
- package/services/wiki-web/.output/server/node_modules/@vue/compiler-core/package.json +58 -0
- package/services/wiki-web/.output/server/node_modules/@vue/compiler-dom/dist/compiler-dom.cjs.prod.js +689 -0
- package/services/wiki-web/.output/server/node_modules/@vue/compiler-dom/package.json +57 -0
- package/services/wiki-web/.output/server/node_modules/@vue/compiler-ssr/dist/compiler-ssr.cjs.js +1413 -0
- package/services/wiki-web/.output/server/node_modules/@vue/compiler-ssr/package.json +34 -0
- package/services/wiki-web/.output/server/node_modules/@vue/reactivity/dist/reactivity.cjs.prod.js +1899 -0
- package/services/wiki-web/.output/server/node_modules/@vue/reactivity/package.json +55 -0
- package/services/wiki-web/.output/server/node_modules/@vue/runtime-core/dist/runtime-core.cjs.prod.js +6940 -0
- package/services/wiki-web/.output/server/node_modules/@vue/runtime-core/package.json +52 -0
- package/services/wiki-web/.output/server/node_modules/@vue/runtime-dom/dist/runtime-dom.cjs.prod.js +1822 -0
- package/services/wiki-web/.output/server/node_modules/@vue/runtime-dom/package.json +60 -0
- package/services/wiki-web/.output/server/node_modules/@vue/server-renderer/dist/server-renderer.cjs.prod.js +896 -0
- package/services/wiki-web/.output/server/node_modules/@vue/server-renderer/package.json +55 -0
- package/services/wiki-web/.output/server/node_modules/@vue/shared/dist/shared.cjs.prod.js +604 -0
- package/services/wiki-web/.output/server/node_modules/@vue/shared/package.json +47 -0
- package/services/wiki-web/.output/server/node_modules/devalue/index.js +4 -0
- package/services/wiki-web/.output/server/node_modules/devalue/package.json +40 -0
- package/services/wiki-web/.output/server/node_modules/devalue/src/base64.js +60 -0
- package/services/wiki-web/.output/server/node_modules/devalue/src/constants.js +12 -0
- package/services/wiki-web/.output/server/node_modules/devalue/src/parse.js +283 -0
- package/services/wiki-web/.output/server/node_modules/devalue/src/stringify.js +401 -0
- package/services/wiki-web/.output/server/node_modules/devalue/src/uneval.js +552 -0
- package/services/wiki-web/.output/server/node_modules/devalue/src/utils.js +160 -0
- package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/decode-codepoint.js +77 -0
- package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/decode.js +568 -0
- package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/generated/decode-data-html.js +7 -0
- package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/generated/decode-data-xml.js +7 -0
- package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/internal/bin-trie-flags.js +21 -0
- package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/internal/decode-shared.js +31 -0
- package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/package.json +3 -0
- package/services/wiki-web/.output/server/node_modules/entities/package.json +120 -0
- package/services/wiki-web/.output/server/node_modules/estree-walker/dist/umd/estree-walker.js +344 -0
- package/services/wiki-web/.output/server/node_modules/estree-walker/package.json +37 -0
- package/services/wiki-web/.output/server/node_modules/hookable/dist/index.mjs +257 -0
- package/services/wiki-web/.output/server/node_modules/hookable/package.json +53 -0
- package/services/wiki-web/.output/server/node_modules/source-map-js/lib/array-set.js +121 -0
- package/services/wiki-web/.output/server/node_modules/source-map-js/lib/base64-vlq.js +140 -0
- package/services/wiki-web/.output/server/node_modules/source-map-js/lib/base64.js +67 -0
- package/services/wiki-web/.output/server/node_modules/source-map-js/lib/binary-search.js +111 -0
- package/services/wiki-web/.output/server/node_modules/source-map-js/lib/mapping-list.js +79 -0
- package/services/wiki-web/.output/server/node_modules/source-map-js/lib/quick-sort.js +132 -0
- package/services/wiki-web/.output/server/node_modules/source-map-js/lib/source-map-consumer.js +1188 -0
- package/services/wiki-web/.output/server/node_modules/source-map-js/lib/source-map-generator.js +444 -0
- package/services/wiki-web/.output/server/node_modules/source-map-js/lib/source-node.js +413 -0
- package/services/wiki-web/.output/server/node_modules/source-map-js/lib/util.js +594 -0
- package/services/wiki-web/.output/server/node_modules/source-map-js/package.json +71 -0
- package/services/wiki-web/.output/server/node_modules/source-map-js/source-map.js +8 -0
- package/services/wiki-web/.output/server/node_modules/ufo/dist/index.mjs +645 -0
- package/services/wiki-web/.output/server/node_modules/ufo/package.json +48 -0
- package/services/wiki-web/.output/server/node_modules/unhead/dist/parser.mjs +508 -0
- package/services/wiki-web/.output/server/node_modules/unhead/dist/plugins.mjs +101 -0
- package/services/wiki-web/.output/server/node_modules/unhead/dist/server.mjs +180 -0
- package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.B3VbJo7Y.mjs +207 -0
- package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.BYvz9V1x.mjs +43 -0
- package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.CaI5ZD4O.mjs +182 -0
- package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.CbpEuj3y.mjs +71 -0
- package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.D4vSFytZ.mjs +236 -0
- package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.DQc16pHI.mjs +196 -0
- package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.TxTMM7cM.mjs +166 -0
- package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.yem5I2v_.mjs +38 -0
- package/services/wiki-web/.output/server/node_modules/unhead/dist/utils.mjs +5 -0
- package/services/wiki-web/.output/server/node_modules/unhead/package.json +105 -0
- package/services/wiki-web/.output/server/node_modules/vue/dist/vue.cjs.js +80 -0
- package/services/wiki-web/.output/server/node_modules/vue/dist/vue.cjs.prod.js +66 -0
- package/services/wiki-web/.output/server/node_modules/vue/index.js +7 -0
- package/services/wiki-web/.output/server/node_modules/vue/index.mjs +1 -0
- package/services/wiki-web/.output/server/node_modules/vue/package.json +112 -0
- package/services/wiki-web/.output/server/node_modules/vue/server-renderer/index.mjs +1 -0
- package/services/wiki-web/.output/server/node_modules/vue-bundle-renderer/dist/runtime.mjs +249 -0
- package/services/wiki-web/.output/server/node_modules/vue-bundle-renderer/package.json +57 -0
- package/services/wiki-web/.output/server/package.json +26 -0
- package/services/wiki-worker/index.cjs +66661 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { hasInstallation } from '../lib/api-config.js';
|
|
2
|
+
import { installDir, resolveInstall } from '../lib/install-root.js';
|
|
3
|
+
import { promptSelect } from '../tui/prompts.js';
|
|
4
|
+
import { run as runProviders } from './config/providers.js';
|
|
5
|
+
import { run as runTemplates } from './config/templates.js';
|
|
6
|
+
import { run as runTokens } from './config/tokens.js';
|
|
7
|
+
import { run as runSources } from './config/sources.js';
|
|
8
|
+
import { run as runGeneration } from './config/generation.js';
|
|
9
|
+
const SUBCOMMANDS = {
|
|
10
|
+
providers: { label: 'Providers', run: runProviders },
|
|
11
|
+
templates: { label: 'Templates', run: runTemplates },
|
|
12
|
+
tokens: { label: 'MCP Tokens', run: runTokens },
|
|
13
|
+
sources: { label: 'Sources', run: runSources },
|
|
14
|
+
generation: { label: 'Generation toggle', run: runGeneration },
|
|
15
|
+
};
|
|
16
|
+
function isSubcommandName(value) {
|
|
17
|
+
return Object.hasOwn(SUBCOMMANDS, value);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `nostale config [subcommand]` — M18 replaces "just re-run the wizard" with a submenu of thin
|
|
21
|
+
* wiki-api REST clients (`./config/*.ts`), each authenticating via `lib/http.ts`'s
|
|
22
|
+
* `login`/`authedFetch`. `nostale config` alone shows an interactive menu; `nostale config
|
|
23
|
+
* providers` (etc.) dispatches straight to that subcommand.
|
|
24
|
+
*/
|
|
25
|
+
export async function configCommand(subcommand) {
|
|
26
|
+
const dir = installDir(resolveInstall());
|
|
27
|
+
if (!hasInstallation(dir)) {
|
|
28
|
+
console.error('No .env found — run `nostale setup` first.');
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (subcommand) {
|
|
33
|
+
if (!isSubcommandName(subcommand)) {
|
|
34
|
+
console.error(`Unknown subcommand "${subcommand}" — expected one of: ${Object.keys(SUBCOMMANDS).join(', ')}`);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
await SUBCOMMANDS[subcommand].run(dir);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const choice = await promptSelect('What would you like to configure?', Object.entries(SUBCOMMANDS).map(([value, { label }]) => ({ label, value: value })));
|
|
42
|
+
await SUBCOMMANDS[choice].run(dir);
|
|
43
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createClient } from '@libsql/client';
|
|
2
|
+
import { readEnvFile } from '../lib/env-writer.js';
|
|
3
|
+
import { installDir, resolveInstall } from '../lib/install-root.js';
|
|
4
|
+
import { envFilePath } from '../lib/paths.js';
|
|
5
|
+
import { isPortFree } from '../lib/ports.js';
|
|
6
|
+
import { SERVICES, isServiceEnabled, statusService } from '../lib/process-manager.js';
|
|
7
|
+
async function checkLibsql(env) {
|
|
8
|
+
const url = env.LIBSQL_URL;
|
|
9
|
+
if (!url)
|
|
10
|
+
return { name: 'LIBSQL_URL', ok: false, detail: 'not set in .env' };
|
|
11
|
+
try {
|
|
12
|
+
const client = createClient({ url, authToken: env.LIBSQL_AUTH_TOKEN || undefined });
|
|
13
|
+
await client.execute('select 1');
|
|
14
|
+
client.close();
|
|
15
|
+
return { name: 'LIBSQL_URL', ok: true, detail: `connected (${url})` };
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
return { name: 'LIBSQL_URL', ok: false, detail: `cannot connect to ${url} — ${err.message}` };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function checkEncryptionKey(env) {
|
|
22
|
+
const key = env.SETTINGS_ENCRYPTION_KEY;
|
|
23
|
+
if (!key)
|
|
24
|
+
return { name: 'SETTINGS_ENCRYPTION_KEY', ok: false, detail: 'missing — run `nostale config` to generate one' };
|
|
25
|
+
if (!/^[0-9a-f]{64}$/.test(key))
|
|
26
|
+
return { name: 'SETTINGS_ENCRYPTION_KEY', ok: false, detail: 'set but not a 32-byte hex key' };
|
|
27
|
+
return { name: 'SETTINGS_ENCRYPTION_KEY', ok: true, detail: 'present' };
|
|
28
|
+
}
|
|
29
|
+
// Without this, apps/wiki-api/src/auth.ts refuses to boot (SECURITY-REVIEW-2026-07-10.md F-01) —
|
|
30
|
+
// checked here too so `nostale doctor` surfaces the same problem before a restart hits it.
|
|
31
|
+
function checkAuthSecret(env) {
|
|
32
|
+
const key = env.BETTER_AUTH_SECRET;
|
|
33
|
+
if (!key)
|
|
34
|
+
return { name: 'BETTER_AUTH_SECRET', ok: false, detail: 'missing — run `nostale config` to generate one' };
|
|
35
|
+
if (key.length < 32)
|
|
36
|
+
return { name: 'BETTER_AUTH_SECRET', ok: false, detail: 'set but shorter than 32 characters' };
|
|
37
|
+
return { name: 'BETTER_AUTH_SECRET', ok: true, detail: 'present' };
|
|
38
|
+
}
|
|
39
|
+
// Empty token means sqld accepts unauthenticated connections on the internal network
|
|
40
|
+
// (SECURITY-REVIEW-2026-07-10.md F-06).
|
|
41
|
+
function checkSqldAuthToken(env) {
|
|
42
|
+
const token = env.SQLD_AUTH_TOKEN;
|
|
43
|
+
if (env.NOSTALE_MODE !== 'server')
|
|
44
|
+
return { name: 'SQLD_AUTH_TOKEN', ok: true, detail: 'not applicable outside server mode' };
|
|
45
|
+
if (!token)
|
|
46
|
+
return { name: 'SQLD_AUTH_TOKEN', ok: false, detail: 'missing — sqld will accept unauthenticated connections; run `nostale config` to generate one' };
|
|
47
|
+
return { name: 'SQLD_AUTH_TOKEN', ok: true, detail: 'present' };
|
|
48
|
+
}
|
|
49
|
+
// Only meaningful for personal mode — server mode's ports are fixed by docker-compose.yml.
|
|
50
|
+
async function checkPortConflicts(env) {
|
|
51
|
+
if (env.NOSTALE_MODE !== 'personal')
|
|
52
|
+
return [];
|
|
53
|
+
const results = [];
|
|
54
|
+
for (const service of SERVICES) {
|
|
55
|
+
if (!isServiceEnabled(service.name, env))
|
|
56
|
+
continue;
|
|
57
|
+
const port = Number(env[service.portEnvKey]);
|
|
58
|
+
if (!port)
|
|
59
|
+
continue;
|
|
60
|
+
if (statusService(service.name).running) {
|
|
61
|
+
results.push({ name: `port ${port} (${service.name})`, ok: true, detail: `held by nostale's own ${service.name}` });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const free = await isPortFree(port);
|
|
65
|
+
results.push({
|
|
66
|
+
name: `port ${port} (${service.name})`,
|
|
67
|
+
ok: free,
|
|
68
|
+
detail: free ? 'free' : 'already in use by another process — pick a different port via `nostale config`',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
export async function doctorCommand() {
|
|
74
|
+
const dir = installDir(resolveInstall());
|
|
75
|
+
const env = readEnvFile(envFilePath(dir));
|
|
76
|
+
if (Object.keys(env).length === 0) {
|
|
77
|
+
console.log('✗ No .env found — run `nostale setup` first.');
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const results = [
|
|
82
|
+
await checkLibsql(env),
|
|
83
|
+
checkEncryptionKey(env),
|
|
84
|
+
checkAuthSecret(env),
|
|
85
|
+
checkSqldAuthToken(env),
|
|
86
|
+
...(await checkPortConflicts(env)),
|
|
87
|
+
];
|
|
88
|
+
for (const r of results)
|
|
89
|
+
console.log(`${r.ok ? '✓' : '✗'} ${r.name} — ${r.detail}`);
|
|
90
|
+
const failed = results.filter((r) => !r.ok);
|
|
91
|
+
if (failed.length > 0) {
|
|
92
|
+
console.log(`\n${failed.length} issue(s) found.`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log('\nAll checks passed.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readEnvFile } from '../lib/env-writer.js';
|
|
3
|
+
import { installDir, resolveInstall } from '../lib/install-root.js';
|
|
4
|
+
import { envFilePath } from '../lib/paths.js';
|
|
5
|
+
import { SERVICES, startAllPersonal } from '../lib/process-manager.js';
|
|
6
|
+
export async function startCommand() {
|
|
7
|
+
const install = resolveInstall();
|
|
8
|
+
const dir = installDir(install);
|
|
9
|
+
const env = readEnvFile(envFilePath(dir));
|
|
10
|
+
if (!env.NOSTALE_MODE) {
|
|
11
|
+
console.log('No .env found — run `nostale setup` first.');
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (env.NOSTALE_MODE === 'server') {
|
|
16
|
+
execFileSync('docker', ['compose', 'up', '-d'], { cwd: dir, stdio: 'inherit' });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const ports = Object.fromEntries(SERVICES.map((s) => [s.name, Number(env[s.portEnvKey])]));
|
|
20
|
+
const started = await startAllPersonal(install, env, ports, { restart: false });
|
|
21
|
+
for (const s of started)
|
|
22
|
+
console.log(`${s.name} on :${ports[s.name]} (pid ${s.pid})`);
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readEnvFile } from '../lib/env-writer.js';
|
|
3
|
+
import { installDir, resolveInstall } from '../lib/install-root.js';
|
|
4
|
+
import { envFilePath } from '../lib/paths.js';
|
|
5
|
+
import { statusAll } from '../lib/process-manager.js';
|
|
6
|
+
export function statusCommand() {
|
|
7
|
+
const dir = installDir(resolveInstall());
|
|
8
|
+
const env = readEnvFile(envFilePath(dir));
|
|
9
|
+
if (env.NOSTALE_MODE === 'server') {
|
|
10
|
+
execFileSync('docker', ['compose', 'ps'], { cwd: dir, stdio: 'inherit' });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
for (const status of statusAll(env)) {
|
|
14
|
+
const state = status.disabled ? 'disabled' : status.running ? `running (pid ${status.pid})` : 'stopped';
|
|
15
|
+
console.log(`${status.name}: ${state}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readEnvFile } from '../lib/env-writer.js';
|
|
3
|
+
import { installDir, resolveInstall } from '../lib/install-root.js';
|
|
4
|
+
import { envFilePath } from '../lib/paths.js';
|
|
5
|
+
import { stopAll } from '../lib/process-manager.js';
|
|
6
|
+
export function stopCommand() {
|
|
7
|
+
const dir = installDir(resolveInstall());
|
|
8
|
+
const env = readEnvFile(envFilePath(dir));
|
|
9
|
+
if (env.NOSTALE_MODE === 'server') {
|
|
10
|
+
execFileSync('docker', ['compose', 'down'], { cwd: dir, stdio: 'inherit' });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
for (const result of stopAll(env)) {
|
|
14
|
+
console.log(`${result.name}: ${result.stopped ? 'stopped' : 'was not running'}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { configCommand } from './commands/config.js';
|
|
4
|
+
import { doctorCommand } from './commands/doctor.js';
|
|
5
|
+
import { setupCommand } from './commands/setup.js';
|
|
6
|
+
import { startCommand } from './commands/start.js';
|
|
7
|
+
import { statusCommand } from './commands/status.js';
|
|
8
|
+
import { stopCommand } from './commands/stop.js';
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program.name('nostale').description('NoStale installer CLI/TUI — setup, process supervision, doctor').version('0.1.0');
|
|
11
|
+
program.command('setup').description('Run the setup wizard (personal or server mode)').action(setupCommand);
|
|
12
|
+
program
|
|
13
|
+
.command('config [subcommand]')
|
|
14
|
+
.description('Configure an existing installation — providers|templates|tokens|sources|generation (interactive menu if omitted)')
|
|
15
|
+
.action((subcommand) => configCommand(subcommand));
|
|
16
|
+
program.command('doctor').description('Diagnose a broken installation').action(doctorCommand);
|
|
17
|
+
program.command('start').description('Start services (personal mode) or the docker compose stack (server mode)').action(startCommand);
|
|
18
|
+
program.command('stop').description('Stop services (personal mode) or the docker compose stack (server mode)').action(stopCommand);
|
|
19
|
+
program.command('status').description('Show service status').action(statusCommand);
|
|
20
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
21
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readEnvFile } from './env-writer.js';
|
|
2
|
+
import { envFilePath } from './paths.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the wiki-api base URL + BetterAuth `origin` header from `.env`. Shared by
|
|
5
|
+
* `config.tsx`'s dispatcher and every `config/*.ts` subcommand so each one just calls
|
|
6
|
+
* `resolveApiConfig(installDir)` instead of re-deriving which env var wins per mode:
|
|
7
|
+
* personal mode writes `WIKI_API_BASE` + `WIKI_WEB_ORIGIN`, server mode writes
|
|
8
|
+
* `WIKI_API_PUBLIC_URL` only (origin falls back to the default `wiki-web` URL BetterAuth's
|
|
9
|
+
* `trustedOrigins` also falls back to — see `apps/wiki-api/src/auth.ts`).
|
|
10
|
+
*/
|
|
11
|
+
export function resolveApiConfig(installDir) {
|
|
12
|
+
const env = readEnvFile(envFilePath(installDir));
|
|
13
|
+
return {
|
|
14
|
+
wikiApiBase: env.WIKI_API_BASE || env.WIKI_API_PUBLIC_URL || 'http://localhost:3001',
|
|
15
|
+
origin: env.WIKI_WEB_ORIGIN || 'http://localhost:3000',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function hasInstallation(installDir) {
|
|
19
|
+
const env = readEnvFile(envFilePath(installDir));
|
|
20
|
+
return Boolean(env.WIKI_API_BASE || env.WIKI_API_PUBLIC_URL);
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
export function parseEnv(content) {
|
|
4
|
+
const vars = {};
|
|
5
|
+
for (const rawLine of content.split('\n')) {
|
|
6
|
+
const line = rawLine.trim();
|
|
7
|
+
if (!line || line.startsWith('#'))
|
|
8
|
+
continue;
|
|
9
|
+
const eq = line.indexOf('=');
|
|
10
|
+
if (eq === -1)
|
|
11
|
+
continue;
|
|
12
|
+
const key = line.slice(0, eq).trim();
|
|
13
|
+
let value = line.slice(eq + 1).trim();
|
|
14
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
15
|
+
value = value.slice(1, -1);
|
|
16
|
+
}
|
|
17
|
+
vars[key] = value;
|
|
18
|
+
}
|
|
19
|
+
return vars;
|
|
20
|
+
}
|
|
21
|
+
export function readEnvFile(path) {
|
|
22
|
+
if (!existsSync(path))
|
|
23
|
+
return {};
|
|
24
|
+
return parseEnv(readFileSync(path, 'utf-8'));
|
|
25
|
+
}
|
|
26
|
+
function serializeValue(value) {
|
|
27
|
+
return /[\s#"]/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
|
|
28
|
+
}
|
|
29
|
+
export function writeEnvFile(path, vars) {
|
|
30
|
+
const lines = Object.entries(vars)
|
|
31
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
32
|
+
.map(([key, value]) => `${key}=${serializeValue(value)}`);
|
|
33
|
+
// .env holds SETTINGS_ENCRYPTION_KEY/LLM_API_KEY in plaintext — mode:0o600 at creation isn't
|
|
34
|
+
// enough on its own since a prior run could have written it looser under a different umask
|
|
35
|
+
// (docs/13 finding #4), so chmod unconditionally on every write, not just the first.
|
|
36
|
+
writeFileSync(path, `# Generated by \`nostale setup\` — see docs/11-installer-and-marketing-site.md\n${lines.join('\n')}\n`, { mode: 0o600 });
|
|
37
|
+
chmodSync(path, 0o600);
|
|
38
|
+
}
|
|
39
|
+
export function generateEncryptionKey() {
|
|
40
|
+
return randomBytes(32).toString('hex');
|
|
41
|
+
}
|
package/dist/lib/http.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { promptPassword, promptText, printStep } from '../tui/prompts.js';
|
|
4
|
+
import { SESSION_FILE } from './paths.js';
|
|
5
|
+
export async function waitForHttp(url, timeoutMs = 20000) {
|
|
6
|
+
const deadline = Date.now() + timeoutMs;
|
|
7
|
+
while (Date.now() < deadline) {
|
|
8
|
+
try {
|
|
9
|
+
await fetch(url);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* BetterAuth's Set-Cookie response is `better-auth.session_token=<token>; Max-Age=...; Path=/;
|
|
20
|
+
* HttpOnly; SameSite=Lax` (confirmed empirically against a real wiki-api + `file:` libSQL — see
|
|
21
|
+
* M18 http.ts work order). We only need the `name=value` pair back on later requests, not the
|
|
22
|
+
* attributes, so strip everything from the first `;` onward. Handles multiple Set-Cookie headers
|
|
23
|
+
* (none expected today, but `getSetCookie()` is the forward-compatible read).
|
|
24
|
+
*/
|
|
25
|
+
function extractCookieHeader(response) {
|
|
26
|
+
const cookies = response.headers.getSetCookie?.() ?? [];
|
|
27
|
+
const pairs = cookies.map((raw) => raw.split(';', 1)[0].trim()).filter(Boolean);
|
|
28
|
+
if (pairs.length > 0)
|
|
29
|
+
return pairs.join('; ');
|
|
30
|
+
const fallback = response.headers.get('set-cookie');
|
|
31
|
+
return fallback ? fallback.split(';', 1)[0].trim() : null;
|
|
32
|
+
}
|
|
33
|
+
function readCachedCookie() {
|
|
34
|
+
return existsSync(SESSION_FILE) ? readFileSync(SESSION_FILE, 'utf-8').trim() || null : null;
|
|
35
|
+
}
|
|
36
|
+
function persistCookie(cookie) {
|
|
37
|
+
mkdirSync(dirname(SESSION_FILE), { recursive: true });
|
|
38
|
+
// mode:0o600 at creation isn't enough alone — a prior run could have left the file looser under
|
|
39
|
+
// a different umask, so chmod unconditionally on every write, same pattern as env-writer.ts
|
|
40
|
+
// (SECURITY-REVIEW-2026-07-10.md F-14). This is a bearer credential.
|
|
41
|
+
writeFileSync(SESSION_FILE, cookie, { mode: 0o600 });
|
|
42
|
+
chmodSync(SESSION_FILE, 0o600);
|
|
43
|
+
}
|
|
44
|
+
function clearCachedCookie() {
|
|
45
|
+
rmSync(SESSION_FILE, { force: true });
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Prompts for email+password and signs in against BetterAuth, same `origin` header requirement
|
|
49
|
+
* as `wizard.ts`'s `provisionFirstUser` (BetterAuth rejects sign-in without an Origin header
|
|
50
|
+
* matching `trustedOrigins`, even for non-browser callers). Persists the resulting session
|
|
51
|
+
* cookie to `SESSION_FILE` (mode 0600 — it's a bearer credential) and returns it.
|
|
52
|
+
*/
|
|
53
|
+
export async function login(wikiApiBase, origin) {
|
|
54
|
+
printStep('Sign in to NoStale');
|
|
55
|
+
const email = await promptText('Email');
|
|
56
|
+
const password = await promptPassword('Password');
|
|
57
|
+
const response = await fetch(`${wikiApiBase}/api/auth/sign-in/email`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'content-type': 'application/json', origin },
|
|
60
|
+
body: JSON.stringify({ email, password }),
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const body = await response.text();
|
|
64
|
+
throw new Error(`sign-in failed (${response.status}): ${body}`);
|
|
65
|
+
}
|
|
66
|
+
const cookie = extractCookieHeader(response);
|
|
67
|
+
if (!cookie)
|
|
68
|
+
throw new Error('sign-in succeeded but no session cookie was returned — cannot proceed');
|
|
69
|
+
persistCookie(cookie);
|
|
70
|
+
return cookie;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Every `nostale config *` subcommand's one entry point for talking to wiki-api: attaches the
|
|
74
|
+
* cached session cookie (from a prior `login()`), and on a 401 (missing/expired session)
|
|
75
|
+
* transparently re-prompts for login and retries the request once. Callers never touch
|
|
76
|
+
* `SESSION_FILE`/`login()` directly.
|
|
77
|
+
*/
|
|
78
|
+
export async function authedFetch(wikiApiBase, origin, path, init = {}) {
|
|
79
|
+
const cookie = readCachedCookie();
|
|
80
|
+
const headers = new Headers(init.headers);
|
|
81
|
+
if (cookie)
|
|
82
|
+
headers.set('cookie', cookie);
|
|
83
|
+
const response = await fetch(`${wikiApiBase}${path}`, { ...init, headers });
|
|
84
|
+
if (response.status !== 401)
|
|
85
|
+
return response;
|
|
86
|
+
clearCachedCookie();
|
|
87
|
+
const freshCookie = await login(wikiApiBase, origin);
|
|
88
|
+
const retryHeaders = new Headers(init.headers);
|
|
89
|
+
retryHeaders.set('cookie', freshCookie);
|
|
90
|
+
return fetch(`${wikiApiBase}${path}`, { ...init, headers: retryHeaders });
|
|
91
|
+
}
|
|
92
|
+
async function assertOk(response) {
|
|
93
|
+
if (response.ok)
|
|
94
|
+
return response;
|
|
95
|
+
const body = await response.text();
|
|
96
|
+
throw new Error(`request failed (${response.status}): ${body}`);
|
|
97
|
+
}
|
|
98
|
+
export async function apiGet(wikiApiBase, origin, path) {
|
|
99
|
+
const response = await authedFetch(wikiApiBase, origin, path);
|
|
100
|
+
return (await assertOk(response)).json();
|
|
101
|
+
}
|
|
102
|
+
export async function apiPost(wikiApiBase, origin, path, body) {
|
|
103
|
+
const response = await authedFetch(wikiApiBase, origin, path, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'content-type': 'application/json' },
|
|
106
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
107
|
+
});
|
|
108
|
+
return (await assertOk(response)).json();
|
|
109
|
+
}
|
|
110
|
+
export async function apiPut(wikiApiBase, origin, path, body) {
|
|
111
|
+
const response = await authedFetch(wikiApiBase, origin, path, {
|
|
112
|
+
method: 'PUT',
|
|
113
|
+
headers: { 'content-type': 'application/json' },
|
|
114
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
115
|
+
});
|
|
116
|
+
return (await assertOk(response)).json();
|
|
117
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { NOSTALE_HOME } from './paths.js';
|
|
5
|
+
import { findRepoRoot } from './repo-root.js';
|
|
6
|
+
// This module lives at <package root>/dist/lib/install-root.js once compiled — three levels
|
|
7
|
+
// down from the package root (dist/lib/install-root.js -> dist/lib -> dist -> package root).
|
|
8
|
+
const packageRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
9
|
+
export function cliPackageRoot() {
|
|
10
|
+
return packageRoot;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Bundled installs ship `<packageRoot>/services/*` alongside `dist/` (docs/14-npm-standalone-
|
|
14
|
+
* distribution.md M21) — their presence is what makes `nostale` runnable with zero prior
|
|
15
|
+
* `nostale` clone. Absent that dir, fall back to the monorepo/tsx dev workflow, which does
|
|
16
|
+
* require a checkout (repo-root.ts, unchanged). Split from resolveInstall() below so this,
|
|
17
|
+
* the actual highest-blast-radius branch in the package, is testable against fake directories
|
|
18
|
+
* instead of this checkout's own real (and test-run-dependent) `services/` presence.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveInstallFrom(packageRoot, cwd) {
|
|
21
|
+
const servicesDir = join(packageRoot, 'services');
|
|
22
|
+
if (existsSync(servicesDir))
|
|
23
|
+
return { mode: 'bundled' };
|
|
24
|
+
return { mode: 'dev', repoRoot: findRepoRoot(cwd) };
|
|
25
|
+
}
|
|
26
|
+
export function resolveInstall() {
|
|
27
|
+
return resolveInstallFrom(packageRoot, process.cwd());
|
|
28
|
+
}
|
|
29
|
+
/** Where `.env` (and, for server mode, docker-compose.yml) live — repo root in dev mode (so
|
|
30
|
+
* Compose's cwd-relative auto-load keeps working there), `~/.nostale` otherwise. */
|
|
31
|
+
export function installDir(install) {
|
|
32
|
+
return install.mode === 'dev' ? install.repoRoot : NOSTALE_HOME;
|
|
33
|
+
}
|
|
34
|
+
const BUNDLED_SERVICE_ENTRY = {
|
|
35
|
+
'wiki-api': 'index.cjs',
|
|
36
|
+
'wiki-worker': 'index.cjs',
|
|
37
|
+
'mcp-server': 'index.cjs',
|
|
38
|
+
'wiki-web': '.output/server/index.mjs',
|
|
39
|
+
};
|
|
40
|
+
/** Absolute path to a bundled service's entry point, e.g. for wiki-api's `--migrate-only` mode
|
|
41
|
+
* (M22) — invoked directly rather than through resolveServiceSource, which is spawn-shaped. */
|
|
42
|
+
export function bundledServiceEntry(name) {
|
|
43
|
+
return join(packageRoot, 'services', name, BUNDLED_SERVICE_ENTRY[name]);
|
|
44
|
+
}
|
|
45
|
+
/** `ServiceDef.command()`'s replacement — resolves what to spawn for a given service, in
|
|
46
|
+
* whichever mode `install` says this is running under. */
|
|
47
|
+
export function resolveServiceSource(install, name, port) {
|
|
48
|
+
if (install.mode === 'bundled') {
|
|
49
|
+
return { cmd: process.execPath, args: [bundledServiceEntry(name)], cwd: join(packageRoot, 'services', name) };
|
|
50
|
+
}
|
|
51
|
+
const { repoRoot } = install;
|
|
52
|
+
const cwd = join(repoRoot, 'apps', name);
|
|
53
|
+
if (name === 'wiki-web') {
|
|
54
|
+
return { cmd: join(repoRoot, 'node_modules/.bin/nuxt'), args: ['dev', '--port', String(port), '--host', '127.0.0.1'], cwd };
|
|
55
|
+
}
|
|
56
|
+
return { cmd: join(repoRoot, 'node_modules/.bin/tsx'), args: ['src/index.ts'], cwd };
|
|
57
|
+
}
|
|
58
|
+
/** Shared dir of pruned tree-sitter grammar `.wasm` files + web-tree-sitter's own runtime wasm,
|
|
59
|
+
* copied there once by `scripts/bundle-services.ts` (M21) — never per-service. */
|
|
60
|
+
export function wasmDir() {
|
|
61
|
+
return join(packageRoot, 'services', 'wasm');
|
|
62
|
+
}
|
|
63
|
+
/** rag-core's `.sql` migration files, copied here by `scripts/bundle-services.ts` (M21) — only
|
|
64
|
+
* wiki-api reads these (on every boot), but set for every bundled service same as wasmDir since
|
|
65
|
+
* it's harmless where unused and keeps startService's bundled-env wiring in one place. */
|
|
66
|
+
export function migrationsDir() {
|
|
67
|
+
return join(packageRoot, 'services', 'migrations');
|
|
68
|
+
}
|
|
69
|
+
/** `@nostale/cli`'s own installed version — the contract (M23/M24) is that a release tags
|
|
70
|
+
* `@nostale/cli@X.Y.Z` and all 4 GHCR images `X.Y.Z` together, so the CLI's own version is
|
|
71
|
+
* always the right image tag to pull for server mode. */
|
|
72
|
+
export function cliVersion() {
|
|
73
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf-8'));
|
|
74
|
+
return pkg.version;
|
|
75
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const NOSTALE_HOME = join(homedir(), '.nostale');
|
|
4
|
+
export const DATA_DIR = join(NOSTALE_HOME, 'data');
|
|
5
|
+
export const PID_DIR = join(NOSTALE_HOME, 'pids');
|
|
6
|
+
export const LOG_DIR = join(NOSTALE_HOME, 'logs');
|
|
7
|
+
export const DEFAULT_DB_PATH = join(DATA_DIR, 'nostale.db');
|
|
8
|
+
export const DEFAULT_WORK_DIR = join(NOSTALE_HOME, 'work');
|
|
9
|
+
export const SESSION_FILE = join(NOSTALE_HOME, 'session');
|
|
10
|
+
export function envFilePath(repoRoot) {
|
|
11
|
+
return join(repoRoot, '.env');
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
export function isPortFree(port) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
const server = createServer();
|
|
5
|
+
server.once('error', () => resolve(false));
|
|
6
|
+
server.once('listening', () => {
|
|
7
|
+
server.close(() => resolve(true));
|
|
8
|
+
});
|
|
9
|
+
server.listen(port, '127.0.0.1');
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/** Walks up from `preferred` until a port is both free on the OS and not already claimed this run. */
|
|
13
|
+
export async function pickPort(preferred, taken = new Set()) {
|
|
14
|
+
let port = preferred;
|
|
15
|
+
while (taken.has(port) || !(await isPortFree(port))) {
|
|
16
|
+
port++;
|
|
17
|
+
}
|
|
18
|
+
return port;
|
|
19
|
+
}
|