@git.zone/tsdoc 2.0.5 → 2.0.6

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.
@@ -9,7 +9,8 @@ export class TypeDoc {
9
9
 
10
10
  // Static
11
11
  public static async isTypeDocDir(dirPathArg: string): Promise<boolean> {
12
- return true;
12
+ return await plugins.fsInstance.file(plugins.path.join(dirPathArg, 'ts/index.ts')).exists()
13
+ || await plugins.fsInstance.file(plugins.path.join(dirPathArg, 'ts_web/index.ts')).exists();
13
14
  }
14
15
 
15
16
  // Instance
@@ -32,26 +33,48 @@ export class TypeDoc {
32
33
  include: [] as string[],
33
34
  };
34
35
  let startDirectory = '';
35
- if (await plugins.fsInstance.directory(plugins.path.join(paths.cwd, './ts')).exists()) {
36
- data.include.push(plugins.path.join(paths.cwd, './ts/**/*'));
36
+ if (await plugins.fsInstance.directory(plugins.path.join(this.typedocDirectory, './ts')).exists()) {
37
+ data.include.push(plugins.path.join(this.typedocDirectory, './ts/**/*'));
37
38
  startDirectory = 'ts';
38
39
  }
39
40
 
40
- if (await plugins.fsInstance.directory(plugins.path.join(paths.cwd, './ts_web')).exists()) {
41
- data.include.push(plugins.path.join(paths.cwd, './ts_web/**/*'));
41
+ if (await plugins.fsInstance.directory(plugins.path.join(this.typedocDirectory, './ts_web')).exists()) {
42
+ data.include.push(plugins.path.join(this.typedocDirectory, './ts_web/**/*'));
42
43
  if (!startDirectory) {
43
44
  startDirectory = 'ts_web';
44
45
  }
45
46
  }
46
47
 
47
- await plugins.fsInstance.file(paths.tsconfigFile).encoding('utf8').write(JSON.stringify(data));
48
- let targetDir = paths.publicDir;
48
+ if (!startDirectory) {
49
+ throw new Error(`No TypeDoc entrypoint found in ${this.typedocDirectory}`);
50
+ }
51
+
52
+ const tempDir = plugins.path.join(this.typedocDirectory, '.nogit', 'tsdoc');
53
+ const tempTsconfigFile = plugins.path.join(tempDir, 'tsconfig.json');
54
+ await plugins.fsInstance.directory(tempDir).recursive().create();
55
+ await plugins.fsInstance.file(tempTsconfigFile).encoding('utf8').write(JSON.stringify(data));
56
+ const publicDir = plugins.path.join(this.typedocDirectory, 'public');
57
+ let targetDir = publicDir;
49
58
  if (options?.publicSubdir) {
50
- targetDir = plugins.path.join(targetDir, options.publicSubdir);
59
+ targetDir = plugins.path.resolve(plugins.path.join(targetDir, options.publicSubdir));
60
+ const resolvedPublicDir = plugins.path.resolve(publicDir);
61
+ if (!targetDir.startsWith(`${resolvedPublicDir}${plugins.path.sep}`) && targetDir !== resolvedPublicDir) {
62
+ throw new Error(`Invalid publicSubdir outside public directory: ${options.publicSubdir}`);
63
+ }
64
+ }
65
+ try {
66
+ const result = await this.smartshellInstance.exec(
67
+ `typedoc --tsconfig ${shellQuote(tempTsconfigFile)} --out ${shellQuote(targetDir)} ${shellQuote(plugins.path.join(this.typedocDirectory, startDirectory, 'index.ts'))}`,
68
+ );
69
+ if (result.exitCode !== 0) {
70
+ throw new Error('typedoc command failed.');
71
+ }
72
+ } finally {
73
+ if (await plugins.fsInstance.file(tempTsconfigFile).exists()) {
74
+ await plugins.fsInstance.file(tempTsconfigFile).delete();
75
+ }
51
76
  }
52
- await this.smartshellInstance.exec(
53
- `typedoc --tsconfig ${paths.tsconfigFile} --out ${targetDir} ${startDirectory}/index.ts`,
54
- );
55
- await plugins.fsInstance.file(paths.tsconfigFile).delete();
56
77
  }
57
78
  }
79
+
80
+ const shellQuote = (value: string): string => `'${value.replaceAll("'", "'\\''")}'`;
package/ts/cli.ts CHANGED
@@ -4,6 +4,54 @@ import { logger } from './logging.js';
4
4
 
5
5
  import { TypeDoc } from './classes.typedoc.js';
6
6
  import { AiDoc } from './classes.aidoc.js';
7
+ import { NoChangesError } from './aidocs_classes/commit.js';
8
+
9
+ const defaultChatGptAuthSources: plugins.smartaiOpenAiChatGptAuth.TOpenAiChatGptAuthSource[] = [
10
+ 'opencode',
11
+ 'codex',
12
+ 'smartai',
13
+ ];
14
+
15
+ const createAiDoc = async (argvArg: any): Promise<AiDoc> => {
16
+ const aidocInstance = new AiDoc(argvArg);
17
+ await aidocInstance.start();
18
+ return aidocInstance;
19
+ };
20
+
21
+ const getSmartcliArgv = (): string[] | undefined => {
22
+ if (process.argv.length === 0) return undefined;
23
+ if (process.argv[0] === process.execPath) return undefined;
24
+ return [process.execPath, 'tsdoc', ...process.argv];
25
+ };
26
+
27
+ const handleAuthCommand = async (argvArg: any): Promise<void> => {
28
+ const subcommand = argvArg._?.[1] ?? 'status';
29
+ if (subcommand === 'login') {
30
+ const deviceCode = await plugins.smartai.requestOpenAiChatGptDeviceCode();
31
+ console.log(`Open ${deviceCode.verificationUrl} and enter code ${deviceCode.userCode}`);
32
+ const tokenData = await plugins.smartai.completeOpenAiChatGptDeviceCodeLogin(deviceCode);
33
+ const authFilePath = plugins.smartaiOpenAiChatGptAuth.getDefaultOpenAiChatGptAuthPath('smartai');
34
+ await plugins.smartaiOpenAiChatGptAuth.writeOpenAiChatGptAuthFile(authFilePath, tokenData, 'smartai');
35
+ logger.log('success', `Stored OpenAI ChatGPT auth at ${authFilePath}`);
36
+ return;
37
+ }
38
+
39
+ const inspections = await plugins.smartaiOpenAiChatGptAuth.inspectOpenAiChatGptAuthSources({
40
+ sources: defaultChatGptAuthSources,
41
+ });
42
+ console.log('OpenAI ChatGPT auth sources:');
43
+ for (const inspection of inspections) {
44
+ const status = inspection.usable
45
+ ? 'usable'
46
+ : inspection.exists
47
+ ? inspection.expired
48
+ ? 'expired'
49
+ : 'not usable'
50
+ : 'missing';
51
+ const account = inspection.email ? ` (${inspection.email})` : '';
52
+ console.log(` ${inspection.source}: ${status}${account} - ${inspection.filePath}`);
53
+ }
54
+ };
7
55
 
8
56
  export const run = async () => {
9
57
  const tsdocCli = new plugins.smartcli.Smartcli();
@@ -28,8 +76,7 @@ export const run = async () => {
28
76
  });
29
77
 
30
78
  tsdocCli.addCommand('aidoc').subscribe(async (argvArg) => {
31
- const aidocInstance = new AiDoc();
32
- await aidocInstance.start();
79
+ const aidocInstance = await createAiDoc(argvArg);
33
80
 
34
81
  logger.log('info', `Generating new readme...`);
35
82
  logger.log('info', `This may take some time...`);
@@ -39,9 +86,12 @@ export const run = async () => {
39
86
  await aidocInstance.buildDescription(paths.cwd);
40
87
  });
41
88
 
89
+ tsdocCli.addCommand('aidocs').subscribe(async (argvArg) => {
90
+ tsdocCli.triggerCommand('aidoc', argvArg);
91
+ });
92
+
42
93
  tsdocCli.addCommand('readme').subscribe(async (argvArg) => {
43
- const aidocInstance = new AiDoc();
44
- await aidocInstance.start();
94
+ const aidocInstance = await createAiDoc(argvArg);
45
95
 
46
96
  logger.log('info', `Generating new readme...`);
47
97
  logger.log('info', `This may take some time...`);
@@ -49,8 +99,7 @@ export const run = async () => {
49
99
  });
50
100
 
51
101
  tsdocCli.addCommand('description').subscribe(async (argvArg) => {
52
- const aidocInstance = new AiDoc();
53
- await aidocInstance.start();
102
+ const aidocInstance = await createAiDoc(argvArg);
54
103
 
55
104
  logger.log('info', `Generating new description and keywords...`);
56
105
  logger.log('info', `This may take some time...`);
@@ -58,17 +107,30 @@ export const run = async () => {
58
107
  });
59
108
 
60
109
  tsdocCli.addCommand('commit').subscribe(async (argvArg) => {
61
- const aidocInstance = new AiDoc();
62
- await aidocInstance.start();
110
+ const aidocInstance = await createAiDoc(argvArg);
63
111
 
64
112
  logger.log('info', `Generating commit message...`);
65
113
  logger.log('info', `This may take some time...`);
66
- const commitObject = await aidocInstance.buildNextCommitObject(paths.cwd);
114
+ let commitObject: Awaited<ReturnType<AiDoc['buildNextCommitObject']>>;
115
+ try {
116
+ commitObject = await aidocInstance.buildNextCommitObject(paths.cwd);
117
+ } catch (error) {
118
+ if (error instanceof NoChangesError || (error as Error).name === 'NoChangesError') {
119
+ logger.log('info', 'No uncommitted changes found.');
120
+ console.log(JSON.stringify({ ok: true, noChanges: true }, null, 2));
121
+ return;
122
+ }
123
+ throw error;
124
+ }
67
125
 
68
126
  logger.log('ok', `Commit message generated:`);
69
127
  console.log(JSON.stringify(commitObject, null, 2));
70
128
  });
71
129
 
130
+ tsdocCli.addCommand('auth').subscribe(async (argvArg) => {
131
+ await handleAuthCommand(argvArg);
132
+ });
133
+
72
134
  tsdocCli.addCommand('test').subscribe((argvArg) => {
73
135
  tsdocCli.triggerCommand('typedoc', argvArg);
74
136
  process.on('exit', async () => {
@@ -76,5 +138,5 @@ export const run = async () => {
76
138
  });
77
139
  });
78
140
 
79
- tsdocCli.startParse();
141
+ tsdocCli.startParse(getSmartcliArgv());
80
142
  };
@@ -0,0 +1,125 @@
1
+ import * as plugins from './plugins.js';
2
+
3
+ const defaultDeniedSegments = new Set([
4
+ '.cache',
5
+ '.git',
6
+ '.next',
7
+ '.nogit',
8
+ '.rpt2_cache',
9
+ 'build',
10
+ 'coverage',
11
+ 'dist',
12
+ 'node_modules',
13
+ 'out',
14
+ ]);
15
+
16
+ const defaultDeniedBasenames = new Set([
17
+ '.npmrc',
18
+ 'bun.lockb',
19
+ 'credentials.json',
20
+ 'deno.lock',
21
+ 'npm-shrinkwrap.json',
22
+ 'package-lock.json',
23
+ 'pnpm-lock.yaml',
24
+ 'yarn.lock',
25
+ ]);
26
+
27
+ const normalizeRelativePath = (rootDir: string, inputPath: string): string => {
28
+ const resolvedRoot = plugins.path.resolve(rootDir);
29
+ const resolvedPath = plugins.path.resolve(
30
+ plugins.path.isAbsolute(inputPath) ? inputPath : plugins.path.join(resolvedRoot, inputPath),
31
+ );
32
+ if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${plugins.path.sep}`)) {
33
+ throw new Error(`Access denied: "${inputPath}" is outside allowed root "${rootDir}"`);
34
+ }
35
+
36
+ const relativePath = plugins.path.relative(resolvedRoot, resolvedPath) || '.';
37
+ assertAllowedRelativePath(relativePath);
38
+ return resolvedPath;
39
+ };
40
+
41
+ const assertAllowedRelativePath = (relativePath: string): void => {
42
+ const normalized = relativePath.split(plugins.path.sep).join('/');
43
+ const parts = normalized.split('/').filter(Boolean);
44
+ const basename = parts.at(-1) ?? normalized;
45
+
46
+ if (basename === '.env' || basename.startsWith('.env.')) {
47
+ throw new Error(`Access denied: ${relativePath} may contain environment secrets.`);
48
+ }
49
+ if (basename.endsWith('.pem') || basename.endsWith('.key') || basename.endsWith('.p12')) {
50
+ throw new Error(`Access denied: ${relativePath} may contain private key material.`);
51
+ }
52
+ if (defaultDeniedBasenames.has(basename)) {
53
+ throw new Error(`Access denied: ${relativePath} is excluded from AI file access.`);
54
+ }
55
+ for (const segment of parts) {
56
+ if (defaultDeniedSegments.has(segment) || segment.startsWith('dist_')) {
57
+ throw new Error(`Access denied: ${relativePath} is excluded from AI file access.`);
58
+ }
59
+ }
60
+ };
61
+
62
+ const listDirectory = async (dirPath: string, recursive = false, prefix = ''): Promise<string[]> => {
63
+ const entries = await plugins.fs.readdir(dirPath, { withFileTypes: true });
64
+ const result: string[] = [];
65
+ for (const entry of entries) {
66
+ const relativeEntryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
67
+ try {
68
+ assertAllowedRelativePath(relativeEntryPath);
69
+ } catch {
70
+ continue;
71
+ }
72
+ result.push(`${relativeEntryPath}${entry.isDirectory() ? '/' : ''}`);
73
+ if (recursive && entry.isDirectory()) {
74
+ result.push(...await listDirectory(plugins.path.join(dirPath, entry.name), true, relativeEntryPath));
75
+ }
76
+ }
77
+ return result;
78
+ };
79
+
80
+ export const createReadOnlyFileSystemTools = (rootDirArg: string): plugins.smartai.ToolSet => {
81
+ const rootDir = plugins.path.resolve(rootDirArg);
82
+
83
+ return {
84
+ read_file: plugins.smartagent.tool({
85
+ description: 'Read file contents within the project. Secret and generated paths are blocked.',
86
+ inputSchema: plugins.smartagent.z.object({
87
+ path: plugins.smartagent.z.string().describe('Absolute or project-relative path to the file'),
88
+ startLine: plugins.smartagent.z.number().optional().describe('First line, 1-indexed and inclusive'),
89
+ endLine: plugins.smartagent.z.number().optional().describe('Last line, 1-indexed and inclusive'),
90
+ }),
91
+ execute: async ({ path: filePath, startLine, endLine }: {
92
+ path: string;
93
+ startLine?: number;
94
+ endLine?: number;
95
+ }) => {
96
+ const resolvedPath = normalizeRelativePath(rootDir, filePath);
97
+ const stat = await plugins.fs.stat(resolvedPath);
98
+ if (!stat.isFile()) {
99
+ throw new Error(`Cannot read non-file path: ${filePath}`);
100
+ }
101
+ const content = await plugins.fs.readFile(resolvedPath, 'utf8');
102
+ const selectedContent = startLine !== undefined || endLine !== undefined
103
+ ? content.split('\n').slice((startLine ?? 1) - 1, endLine).join('\n')
104
+ : content;
105
+ return plugins.smartagent.truncateOutput(selectedContent).content;
106
+ },
107
+ }),
108
+ list_directory: plugins.smartagent.tool({
109
+ description: 'List project files and directories. Secret and generated paths are omitted.',
110
+ inputSchema: plugins.smartagent.z.object({
111
+ path: plugins.smartagent.z.string().describe('Absolute or project-relative directory path to list'),
112
+ recursive: plugins.smartagent.z.boolean().optional().describe('List recursively'),
113
+ }),
114
+ execute: async ({ path: dirPath, recursive }: { path: string; recursive?: boolean }) => {
115
+ const resolvedPath = normalizeRelativePath(rootDir, dirPath);
116
+ const stat = await plugins.fs.stat(resolvedPath);
117
+ if (!stat.isDirectory()) {
118
+ throw new Error(`Cannot list non-directory path: ${dirPath}`);
119
+ }
120
+ const entries = await listDirectory(resolvedPath, recursive === true);
121
+ return plugins.smartagent.truncateOutput(entries.join('\n')).content;
122
+ },
123
+ }),
124
+ };
125
+ };
package/ts/index.ts CHANGED
@@ -10,3 +10,4 @@ export const runCli = async () => {
10
10
 
11
11
  // exports
12
12
  export * from './classes.aidoc.js';
13
+ export { NoChangesError } from './aidocs_classes/commit.js';
package/ts/plugins.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  // node native
2
+ import * as fs from 'node:fs/promises';
2
3
  import * as path from 'path';
3
4
 
4
- export { path };
5
+ export { fs, path };
5
6
 
6
7
  // pushrocks scope
7
8
  import * as smartconfig from '@push.rocks/smartconfig';
8
9
  import * as qenv from '@push.rocks/qenv';
9
10
  import * as smartagent from '@push.rocks/smartagent';
11
+ import * as smartagentCompaction from '@push.rocks/smartagent/compaction';
10
12
  import * as smartagentTools from '@push.rocks/smartagent/tools';
11
13
  import * as smartai from '@push.rocks/smartai';
14
+ import * as smartaiOpenAiChatGptAuth from '@push.rocks/smartai/openai-chatgpt-auth';
12
15
  import * as smartcli from '@push.rocks/smartcli';
13
16
  import * as smartdelay from '@push.rocks/smartdelay';
14
17
  import * as smartfile from '@push.rocks/smartfile';
@@ -25,8 +28,10 @@ export {
25
28
  smartconfig,
26
29
  qenv,
27
30
  smartagent,
31
+ smartagentCompaction,
28
32
  smartagentTools,
29
33
  smartai,
34
+ smartaiOpenAiChatGptAuth,
30
35
  smartcli,
31
36
  smartdelay,
32
37
  smartfile,