dsh-gh-pages-artifacts 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 +227 -0
- package/assets/artifact-pages/SKILL.md +84 -0
- package/bin/setup.mjs +270 -0
- package/client/client.js +564 -0
- package/cordis.patch.yml +15 -0
- package/icon.svg +6 -0
- package/lib/approval.d.ts +26 -0
- package/lib/approval.js +59 -0
- package/lib/config.d.ts +204 -0
- package/lib/config.js +241 -0
- package/lib/content.d.ts +43 -0
- package/lib/content.js +142 -0
- package/lib/github.d.ts +78 -0
- package/lib/github.js +190 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +48 -0
- package/lib/manifest.d.ts +88 -0
- package/lib/manifest.js +172 -0
- package/lib/names.d.ts +17 -0
- package/lib/names.js +17 -0
- package/lib/prompt.d.ts +15 -0
- package/lib/prompt.js +33 -0
- package/lib/registry.d.ts +70 -0
- package/lib/registry.js +151 -0
- package/lib/render.d.ts +43 -0
- package/lib/render.js +128 -0
- package/lib/service.d.ts +292 -0
- package/lib/service.js +1216 -0
- package/lib/skill.d.ts +16 -0
- package/lib/skill.js +59 -0
- package/lib/store.d.ts +167 -0
- package/lib/store.js +379 -0
- package/lib/tools.d.ts +10 -0
- package/lib/tools.js +320 -0
- package/lib/visibility.d.ts +34 -0
- package/lib/visibility.js +81 -0
- package/lib/web-routes.d.ts +29 -0
- package/lib/web-routes.js +84 -0
- package/locale/en.json +6 -0
- package/package.json +130 -0
package/lib/approval.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask the user before a publish, update, or delete, following the configured mode. With
|
|
3
|
+
* 'unless-full-access' the prompt is skipped only in sessions that run with the danger-full-access
|
|
4
|
+
* sandbox and never prompt for approval (the "Full access" preset). The gate runs
|
|
5
|
+
* inside the tool body, after arguments and the target URL are known, so the prompt names the
|
|
6
|
+
* exact URL. Only an explicit 'allowed-once' lets the change through.
|
|
7
|
+
* @param ctx - plugin context.
|
|
8
|
+
* @param mode - configured approval mode.
|
|
9
|
+
* @param exec - tool execution.
|
|
10
|
+
* @param ask - request text.
|
|
11
|
+
* @throws Error with a model-facing explanation when approval is not granted.
|
|
12
|
+
*/
|
|
13
|
+
export async function requireApproval(ctx, mode, exec, ask) {
|
|
14
|
+
if (mode === 'off')
|
|
15
|
+
return;
|
|
16
|
+
const agent = exec.agent;
|
|
17
|
+
const approval = ctx.get('approval');
|
|
18
|
+
const policy = approval === undefined || agent === undefined
|
|
19
|
+
? undefined
|
|
20
|
+
: approval.overrideOf(agent.session) ?? approval.config.policy ?? 'ask';
|
|
21
|
+
if (mode === 'unless-full-access') {
|
|
22
|
+
// Full access means the danger-full-access sandbox *and* no approval prompts, the way the
|
|
23
|
+
// shipped "Full access" preset is defined. Auto mode keeps prompting, so it is asked here too.
|
|
24
|
+
const sandbox = ctx.get('sandboxPolicy')?.resolve(agent === undefined ? {} : { session: agent.session });
|
|
25
|
+
if (sandbox?.mode === 'danger-full-access' && (policy === undefined || policy === 'never'))
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (approval === undefined || agent === undefined || policy === undefined) {
|
|
29
|
+
throw new Error(`${ask.toolName} needs the user's approval, but this harness has no approval channel. `
|
|
30
|
+
+ 'Ask the user to publish from an interactive session, or to set the plugin option approval: off.');
|
|
31
|
+
}
|
|
32
|
+
if (policy === 'never') {
|
|
33
|
+
const delegated = (agent.session.header.delegationDepth ?? 0) > 0;
|
|
34
|
+
throw new Error(delegated
|
|
35
|
+
? `${ask.toolName} needs the user's approval, which a delegated subagent cannot request. Return the content to the parent agent so it can publish.`
|
|
36
|
+
: `${ask.toolName} needs the user's approval, but this session rejects approval requests automatically. Ask the user to change the permission mode, then retry.`);
|
|
37
|
+
}
|
|
38
|
+
const outcome = await approval.request({
|
|
39
|
+
agent,
|
|
40
|
+
toolName: ask.toolName,
|
|
41
|
+
callId: exec.callId,
|
|
42
|
+
reason: ask.reason,
|
|
43
|
+
displayReason: { en: ask.display },
|
|
44
|
+
signal: exec.signal,
|
|
45
|
+
});
|
|
46
|
+
switch (outcome) {
|
|
47
|
+
case 'allowed-once':
|
|
48
|
+
exec.signal.throwIfAborted();
|
|
49
|
+
return;
|
|
50
|
+
case 'rejected':
|
|
51
|
+
throw new Error(`The user declined: ${ask.reason}. Do not retry unless the user asks again.`);
|
|
52
|
+
case 'cancelled':
|
|
53
|
+
throw new Error(`The approval request was cancelled: ${ask.reason}`);
|
|
54
|
+
case 'unavailable':
|
|
55
|
+
throw new Error(`${ask.toolName} needs the user's approval, but no approval channel is available right now (for example a headless run).`);
|
|
56
|
+
default:
|
|
57
|
+
throw new Error(`${ask.toolName} was not approved`);
|
|
58
|
+
}
|
|
59
|
+
}
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
/** How publishing, updating, and deleting artifacts is approved. */
|
|
3
|
+
export type ApprovalMode = 'unless-full-access' | 'always' | 'off';
|
|
4
|
+
/** Which artifact tools delegated subagents may see. */
|
|
5
|
+
export type SubagentAccess = 'read-only' | 'full' | 'none';
|
|
6
|
+
/**
|
|
7
|
+
* Where new artifacts go: 'shared' puts every artifact in its own folder of one linked repository;
|
|
8
|
+
* 'per-artifact' creates a new repository (and Pages site) for each new artifact.
|
|
9
|
+
*/
|
|
10
|
+
export type RepoStrategy = 'shared' | 'per-artifact';
|
|
11
|
+
/** A GitHub repository. */
|
|
12
|
+
export interface RepoRef {
|
|
13
|
+
readonly owner: string;
|
|
14
|
+
readonly repo: string;
|
|
15
|
+
}
|
|
16
|
+
/** Raw plugin configuration as validated by {@link Config}. */
|
|
17
|
+
export interface Config {
|
|
18
|
+
/** GitHub user or organization that owns the Pages repository. Defaults to the token's user. */
|
|
19
|
+
owner?: string | undefined;
|
|
20
|
+
/** Repository that hosts the artifacts. */
|
|
21
|
+
repo: string;
|
|
22
|
+
/** The shared repository as `owner/name` or a remote URL; overrides owner and repo for the shared repository. */
|
|
23
|
+
repository?: string | undefined;
|
|
24
|
+
/** Whether new artifacts share one repository or each get a new one. */
|
|
25
|
+
repoStrategy: RepoStrategy;
|
|
26
|
+
/** Name prefix of repositories created by the per-artifact strategy. */
|
|
27
|
+
repoPrefix: string;
|
|
28
|
+
/** Visibility of repositories created by the per-artifact strategy. */
|
|
29
|
+
repoVisibility: 'public' | 'private';
|
|
30
|
+
/** Folder for the local registry of published artifacts; defaults to $DSH_HOME/gh-pages-artifacts. */
|
|
31
|
+
registryDir?: string | undefined;
|
|
32
|
+
/** Branch GitHub Pages publishes from. */
|
|
33
|
+
branch: string;
|
|
34
|
+
/** Pages source folder inside the branch: '' for the root or 'docs'. */
|
|
35
|
+
siteDir: '' | 'docs';
|
|
36
|
+
/** Folder under the site root that holds artifacts; '' places them at the site root. */
|
|
37
|
+
pathPrefix: string;
|
|
38
|
+
/** Public base URL of the site, for custom domains. Discovered from the Pages API when unset. */
|
|
39
|
+
baseUrl?: string | undefined;
|
|
40
|
+
/** Name of the credential reference (environment variable) holding the GitHub token. */
|
|
41
|
+
tokenEnv: string;
|
|
42
|
+
/** GitHub REST API base URL; change it only for GitHub Enterprise Server. */
|
|
43
|
+
apiBaseUrl: string;
|
|
44
|
+
/** When publish, update, and delete ask the user first. */
|
|
45
|
+
approval: ApprovalMode;
|
|
46
|
+
/** Agent presets (modes) that never see the artifact tools. */
|
|
47
|
+
hideFromPresets: string[];
|
|
48
|
+
/** Which artifact tools delegated subagents see. */
|
|
49
|
+
subagentAccess: SubagentAccess;
|
|
50
|
+
/** Add `<meta name="robots" content="noindex, nofollow">` to published pages. */
|
|
51
|
+
noindex: boolean;
|
|
52
|
+
/** Content-Security-Policy injected as a meta tag; '' disables injection. */
|
|
53
|
+
csp: string;
|
|
54
|
+
/** Refuse to publish text that looks like a credential (tokens, private keys). */
|
|
55
|
+
blockSecrets: boolean;
|
|
56
|
+
/** Maximum total bytes of one publish (page plus assets). */
|
|
57
|
+
maxPublishBytes: number;
|
|
58
|
+
/** Optional author and committer identity for commits; defaults to the token's user. */
|
|
59
|
+
commitAuthor?: {
|
|
60
|
+
name?: string | undefined;
|
|
61
|
+
email?: string | undefined;
|
|
62
|
+
} | undefined;
|
|
63
|
+
/** Add a short system-prompt section describing the artifact tools. */
|
|
64
|
+
promptGuidance: boolean;
|
|
65
|
+
/** Register the bundled `artifact-pages` design skill. */
|
|
66
|
+
bundledSkill: boolean;
|
|
67
|
+
}
|
|
68
|
+
/** Schema validated by Cordis before `apply`. */
|
|
69
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
70
|
+
owner: z<string, string, "volatile">;
|
|
71
|
+
repo: z<string, string, "volatile-defined">;
|
|
72
|
+
repository: z<string, string, "volatile">;
|
|
73
|
+
repoStrategy: z<"shared" | "per-artifact", "shared" | "per-artifact", "volatile-defined">;
|
|
74
|
+
repoPrefix: z<string, string, "volatile-defined">;
|
|
75
|
+
repoVisibility: z<"public" | "private", "public" | "private", "volatile-defined">;
|
|
76
|
+
registryDir: z<string, string, "plain">;
|
|
77
|
+
branch: z<string, string, "volatile-defined">;
|
|
78
|
+
siteDir: z<"" | "docs", "" | "docs", "defined">;
|
|
79
|
+
pathPrefix: z<string, string, "defined">;
|
|
80
|
+
baseUrl: z<string, string, "volatile">;
|
|
81
|
+
tokenEnv: z<string, string, "volatile-defined">;
|
|
82
|
+
apiBaseUrl: z<string, string, "defined">;
|
|
83
|
+
approval: z<"unless-full-access" | "always" | "off", "unless-full-access" | "always" | "off", "volatile-defined">;
|
|
84
|
+
hideFromPresets: z<string[], string[], "defined">;
|
|
85
|
+
subagentAccess: z<"read-only" | "full" | "none", "read-only" | "full" | "none", "defined">;
|
|
86
|
+
noindex: z<boolean, boolean, "volatile-defined">;
|
|
87
|
+
csp: z<string, string, "defined">;
|
|
88
|
+
blockSecrets: z<boolean, boolean, "volatile-defined">;
|
|
89
|
+
maxPublishBytes: z<number, number, "volatile-defined">;
|
|
90
|
+
commitAuthor: z<NoInfer<Schemastery.ObjectS<NoInfer<{
|
|
91
|
+
name: z<string, string, "plain">;
|
|
92
|
+
email: z<string, string, "plain">;
|
|
93
|
+
}>>>, NoInfer<Schemastery.ObjectT<NoInfer<{
|
|
94
|
+
name: z<string, string, "plain">;
|
|
95
|
+
email: z<string, string, "plain">;
|
|
96
|
+
}>>>, "volatile">;
|
|
97
|
+
promptGuidance: z<boolean, boolean, "defined">;
|
|
98
|
+
bundledSkill: z<boolean, boolean, "defined">;
|
|
99
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
100
|
+
owner: z<string, string, "volatile">;
|
|
101
|
+
repo: z<string, string, "volatile-defined">;
|
|
102
|
+
repository: z<string, string, "volatile">;
|
|
103
|
+
repoStrategy: z<"shared" | "per-artifact", "shared" | "per-artifact", "volatile-defined">;
|
|
104
|
+
repoPrefix: z<string, string, "volatile-defined">;
|
|
105
|
+
repoVisibility: z<"public" | "private", "public" | "private", "volatile-defined">;
|
|
106
|
+
registryDir: z<string, string, "plain">;
|
|
107
|
+
branch: z<string, string, "volatile-defined">;
|
|
108
|
+
siteDir: z<"" | "docs", "" | "docs", "defined">;
|
|
109
|
+
pathPrefix: z<string, string, "defined">;
|
|
110
|
+
baseUrl: z<string, string, "volatile">;
|
|
111
|
+
tokenEnv: z<string, string, "volatile-defined">;
|
|
112
|
+
apiBaseUrl: z<string, string, "defined">;
|
|
113
|
+
approval: z<"unless-full-access" | "always" | "off", "unless-full-access" | "always" | "off", "volatile-defined">;
|
|
114
|
+
hideFromPresets: z<string[], string[], "defined">;
|
|
115
|
+
subagentAccess: z<"read-only" | "full" | "none", "read-only" | "full" | "none", "defined">;
|
|
116
|
+
noindex: z<boolean, boolean, "volatile-defined">;
|
|
117
|
+
csp: z<string, string, "defined">;
|
|
118
|
+
blockSecrets: z<boolean, boolean, "volatile-defined">;
|
|
119
|
+
maxPublishBytes: z<number, number, "volatile-defined">;
|
|
120
|
+
commitAuthor: z<NoInfer<Schemastery.ObjectS<NoInfer<{
|
|
121
|
+
name: z<string, string, "plain">;
|
|
122
|
+
email: z<string, string, "plain">;
|
|
123
|
+
}>>>, NoInfer<Schemastery.ObjectT<NoInfer<{
|
|
124
|
+
name: z<string, string, "plain">;
|
|
125
|
+
email: z<string, string, "plain">;
|
|
126
|
+
}>>>, "volatile">;
|
|
127
|
+
promptGuidance: z<boolean, boolean, "defined">;
|
|
128
|
+
bundledSkill: z<boolean, boolean, "defined">;
|
|
129
|
+
}>>, "plain">;
|
|
130
|
+
/** Config as the Loader passes it: fields declared `.volatile()` arrive as live references. */
|
|
131
|
+
export type LiveConfig = {
|
|
132
|
+
[K in keyof Config]: Config[K] | {
|
|
133
|
+
get(): Config[K];
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Read the current values of a live config; volatile fields can change while the plugin runs
|
|
138
|
+
* (for example from its settings page), so callers read them per operation.
|
|
139
|
+
* @param config - config as passed to `apply`.
|
|
140
|
+
* @returns plain values.
|
|
141
|
+
*/
|
|
142
|
+
export declare function unwrapConfig(config: Config | LiveConfig): Config;
|
|
143
|
+
/** Validated, normalized settings shared by every module. */
|
|
144
|
+
export interface Settings {
|
|
145
|
+
readonly owner: string | undefined;
|
|
146
|
+
readonly repo: string;
|
|
147
|
+
/** Shared repository parsed from `repository`, when set. */
|
|
148
|
+
readonly repository: RepoRef | undefined;
|
|
149
|
+
readonly repoStrategy: RepoStrategy;
|
|
150
|
+
readonly repoPrefix: string;
|
|
151
|
+
readonly repoVisibility: 'public' | 'private';
|
|
152
|
+
readonly registryDir: string;
|
|
153
|
+
readonly branch: string;
|
|
154
|
+
readonly siteDir: string;
|
|
155
|
+
readonly pathPrefix: string;
|
|
156
|
+
readonly baseUrl: string | undefined;
|
|
157
|
+
readonly tokenEnv: string;
|
|
158
|
+
readonly apiBaseUrl: string;
|
|
159
|
+
readonly approval: ApprovalMode;
|
|
160
|
+
readonly hideFromPresets: readonly string[];
|
|
161
|
+
readonly subagentAccess: SubagentAccess;
|
|
162
|
+
readonly noindex: boolean;
|
|
163
|
+
readonly csp: string;
|
|
164
|
+
readonly blockSecrets: boolean;
|
|
165
|
+
readonly maxPublishBytes: number;
|
|
166
|
+
readonly commitAuthor: {
|
|
167
|
+
readonly name: string;
|
|
168
|
+
readonly email: string;
|
|
169
|
+
} | undefined;
|
|
170
|
+
readonly promptGuidance: boolean;
|
|
171
|
+
readonly bundledSkill: boolean;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check the cross-field rules the schema cannot express and normalize paths and URLs.
|
|
175
|
+
* @param config - schema-validated configuration.
|
|
176
|
+
* @returns normalized settings.
|
|
177
|
+
* @throws Error naming the offending option.
|
|
178
|
+
*/
|
|
179
|
+
export declare function normalizeConfig(input: Config | LiveConfig): Settings;
|
|
180
|
+
/**
|
|
181
|
+
* Parse a repository reference: `owner/name`, `https://<host>/owner/name(.git)`,
|
|
182
|
+
* `git@<host>:owner/name(.git)`, or `ssh://git@<host>/owner/name(.git)`. The host must be
|
|
183
|
+
* github.com, or the GitHub Enterprise host behind `apiBaseUrl`.
|
|
184
|
+
* @param input - user text.
|
|
185
|
+
* @param apiBaseUrl - normalized API base URL.
|
|
186
|
+
* @returns owner and repository name.
|
|
187
|
+
* @throws Error explaining the accepted forms.
|
|
188
|
+
*/
|
|
189
|
+
export declare function parseRepository(input: string, apiBaseUrl: string): RepoRef;
|
|
190
|
+
/**
|
|
191
|
+
* Normalize a relative directory option such as `a` or `pages/a` to slash-joined safe segments.
|
|
192
|
+
* @param value - raw option value.
|
|
193
|
+
* @param option - option name for error messages.
|
|
194
|
+
* @returns the normalized path, '' for the root.
|
|
195
|
+
*/
|
|
196
|
+
export declare function normalizeRelativeDir(value: string, option: string): string;
|
|
197
|
+
/**
|
|
198
|
+
* Validate an http(s) URL option and strip its trailing slash.
|
|
199
|
+
* @param value - raw option value.
|
|
200
|
+
* @param option - option name for error messages.
|
|
201
|
+
* @param carriesToken - whether the token is sent to this URL, which then must be HTTPS unless loopback.
|
|
202
|
+
* @returns the URL without a trailing slash.
|
|
203
|
+
*/
|
|
204
|
+
export declare function normalizeUrl(value: string, option: string, carriesToken: boolean): string;
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/** Plugin configuration schema and the normalized settings the runtime reads. */
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { isAbsolute, join } from 'node:path';
|
|
4
|
+
import z from '@deepseek-ai/schemastery';
|
|
5
|
+
const OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
|
6
|
+
const REPO_PATTERN = /^[A-Za-z0-9._-]{1,100}$/;
|
|
7
|
+
const BRANCH_PATTERN = /^(?!\/)(?!.*\/\/)(?!.*\.\.)(?!.*@\{)(?!.*\/$)(?!.*\.lock$)[A-Za-z0-9._\/-]{1,200}$/;
|
|
8
|
+
const SEGMENT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
9
|
+
/** Schema validated by Cordis before `apply`. */
|
|
10
|
+
export const Config = z.object({
|
|
11
|
+
owner: z.string().pattern(OWNER_PATTERN)
|
|
12
|
+
.description('GitHub user or organization owning the Pages repository. Defaults to the user the token belongs to.').volatile(),
|
|
13
|
+
repo: z.string().pattern(REPO_PATTERN).default('dsh-artifacts')
|
|
14
|
+
.description('Repository that hosts the artifacts. Use a dedicated repository, never your <owner>.github.io site.').volatile(),
|
|
15
|
+
repository: z.string()
|
|
16
|
+
.description('Shared repository as owner/name or a remote URL (https://github.com/owner/name.git or git@github.com:owner/name.git). Overrides owner and repo for the shared repository.').volatile(),
|
|
17
|
+
repoStrategy: z.union(['shared', 'per-artifact']).default('shared')
|
|
18
|
+
.description('shared: every artifact gets a folder in one repository; per-artifact: each new artifact gets its own new repository and Pages site.').volatile(),
|
|
19
|
+
repoPrefix: z.string().pattern(/^[A-Za-z0-9._-]{0,40}$/).default('artifact-')
|
|
20
|
+
.description('Name prefix of repositories the per-artifact strategy creates.').volatile(),
|
|
21
|
+
repoVisibility: z.union(['public', 'private']).default('public')
|
|
22
|
+
.description('Visibility of repositories the per-artifact strategy creates. Pages on private repositories needs a paid plan; the pages are public either way.').volatile(),
|
|
23
|
+
registryDir: z.string()
|
|
24
|
+
.description('Folder for the local registry and index page of published artifacts. Defaults to $DSH_HOME/gh-pages-artifacts.'),
|
|
25
|
+
branch: z.string().pattern(BRANCH_PATTERN).default('gh-pages')
|
|
26
|
+
.description('Branch GitHub Pages publishes from. It is created on first publish when missing.').volatile(),
|
|
27
|
+
siteDir: z.union(['', 'docs']).default('')
|
|
28
|
+
.description("Pages source folder: '' for the branch root or 'docs'."),
|
|
29
|
+
pathPrefix: z.string().default('')
|
|
30
|
+
.description("Folder under the site root that holds artifacts, for example 'a'. '' puts each artifact at <site>/<id>/."),
|
|
31
|
+
baseUrl: z.string()
|
|
32
|
+
.description('Public site URL, e.g. https://pages.example.com. Discovered from the Pages API when unset.').volatile(),
|
|
33
|
+
tokenEnv: z.string().role('credential-ref').default('GH_PAGES_TOKEN')
|
|
34
|
+
.description('Credential reference (environment variable name) holding a GitHub token with Contents read/write on the repository.').volatile(),
|
|
35
|
+
apiBaseUrl: z.string().default('https://api.github.com')
|
|
36
|
+
.description('GitHub REST API base URL. Change it only for GitHub Enterprise Server.'),
|
|
37
|
+
approval: z.union(['unless-full-access', 'always', 'off']).default('unless-full-access')
|
|
38
|
+
.description('unless-full-access: ask unless the session runs with danger-full-access; always: ask every time; off: never ask.').volatile(),
|
|
39
|
+
hideFromPresets: z.array(z.string()).default(['minimal'])
|
|
40
|
+
.description('Agent presets (modes) that never see the artifact tools.'),
|
|
41
|
+
subagentAccess: z.union(['read-only', 'full', 'none']).default('read-only')
|
|
42
|
+
.description('read-only: subagents may list and read but not publish or delete; full: all tools; none: no artifact tools.'),
|
|
43
|
+
noindex: z.boolean().default(true)
|
|
44
|
+
.description('Ask search engines not to index published pages.').volatile(),
|
|
45
|
+
csp: z.string().default("object-src 'none'; base-uri 'none'")
|
|
46
|
+
.description("Content-Security-Policy meta tag injected into every page. '' disables it."),
|
|
47
|
+
blockSecrets: z.boolean().default(true)
|
|
48
|
+
.description('Refuse to publish text that looks like a credential.').volatile(),
|
|
49
|
+
maxPublishBytes: z.natural().min(1024).default(10 * 1024 * 1024)
|
|
50
|
+
.description('Maximum total bytes of one publish, page plus assets.').volatile(),
|
|
51
|
+
commitAuthor: z.object({
|
|
52
|
+
name: z.string().description('Author name.'),
|
|
53
|
+
email: z.string().description('Author email.'),
|
|
54
|
+
}).description('Commit author and committer (both name and email). Defaults to the token user.').volatile(),
|
|
55
|
+
promptGuidance: z.boolean().default(true)
|
|
56
|
+
.description('Add a short system-prompt section that explains the artifact tools.'),
|
|
57
|
+
bundledSkill: z.boolean().default(true)
|
|
58
|
+
.description('Register the bundled artifact-pages design skill.'),
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Read the current values of a live config; volatile fields can change while the plugin runs
|
|
62
|
+
* (for example from its settings page), so callers read them per operation.
|
|
63
|
+
* @param config - config as passed to `apply`.
|
|
64
|
+
* @returns plain values.
|
|
65
|
+
*/
|
|
66
|
+
export function unwrapConfig(config) {
|
|
67
|
+
const plain = {};
|
|
68
|
+
for (const [key, value] of Object.entries(config)) {
|
|
69
|
+
plain[key] = typeof value === 'object' && value !== null && typeof value.get === 'function'
|
|
70
|
+
? value.get()
|
|
71
|
+
: value;
|
|
72
|
+
}
|
|
73
|
+
return plain;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check the cross-field rules the schema cannot express and normalize paths and URLs.
|
|
77
|
+
* @param config - schema-validated configuration.
|
|
78
|
+
* @returns normalized settings.
|
|
79
|
+
* @throws Error naming the offending option.
|
|
80
|
+
*/
|
|
81
|
+
export function normalizeConfig(input) {
|
|
82
|
+
const config = unwrapConfig(input);
|
|
83
|
+
const pathPrefix = normalizeRelativeDir(config.pathPrefix, 'pathPrefix');
|
|
84
|
+
const apiBaseUrl = normalizeUrl(config.apiBaseUrl, 'apiBaseUrl', true);
|
|
85
|
+
const baseUrl = config.baseUrl === undefined || config.baseUrl.trim() === ''
|
|
86
|
+
? undefined
|
|
87
|
+
: normalizeUrl(config.baseUrl, 'baseUrl', false);
|
|
88
|
+
if (/[\s;]/.test(config.tokenEnv) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(config.tokenEnv)) {
|
|
89
|
+
throw new Error(`gh-pages-artifacts: tokenEnv must be an environment variable name, got "${config.tokenEnv}"`);
|
|
90
|
+
}
|
|
91
|
+
const author = normalizeAuthor(config.commitAuthor);
|
|
92
|
+
if (/[\r\n]/.test(config.csp))
|
|
93
|
+
throw new Error('gh-pages-artifacts: csp must be a single line');
|
|
94
|
+
return {
|
|
95
|
+
owner: config.owner === undefined || config.owner === '' ? undefined : config.owner,
|
|
96
|
+
repo: config.repo,
|
|
97
|
+
repository: config.repository === undefined || config.repository.trim() === '' ? undefined : parseRepository(config.repository, apiBaseUrl),
|
|
98
|
+
repoStrategy: config.repoStrategy,
|
|
99
|
+
repoPrefix: config.repoPrefix,
|
|
100
|
+
repoVisibility: config.repoVisibility,
|
|
101
|
+
registryDir: normalizeRegistryDir(config.registryDir),
|
|
102
|
+
branch: config.branch,
|
|
103
|
+
siteDir: config.siteDir,
|
|
104
|
+
pathPrefix,
|
|
105
|
+
baseUrl,
|
|
106
|
+
tokenEnv: config.tokenEnv,
|
|
107
|
+
apiBaseUrl,
|
|
108
|
+
approval: config.approval,
|
|
109
|
+
hideFromPresets: [...config.hideFromPresets],
|
|
110
|
+
subagentAccess: config.subagentAccess,
|
|
111
|
+
noindex: config.noindex,
|
|
112
|
+
csp: config.csp.trim(),
|
|
113
|
+
blockSecrets: config.blockSecrets,
|
|
114
|
+
maxPublishBytes: config.maxPublishBytes,
|
|
115
|
+
commitAuthor: author,
|
|
116
|
+
promptGuidance: config.promptGuidance,
|
|
117
|
+
bundledSkill: config.bundledSkill,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function normalizeAuthor(author) {
|
|
121
|
+
const name = author?.name?.trim() ?? '';
|
|
122
|
+
const email = author?.email?.trim() ?? '';
|
|
123
|
+
if (name === '' && email === '')
|
|
124
|
+
return undefined;
|
|
125
|
+
if (name === '' || !/^[^\s@<>]+@[^\s@<>]+$/.test(email)) {
|
|
126
|
+
throw new Error('gh-pages-artifacts: commitAuthor needs both a non-empty name and a valid email');
|
|
127
|
+
}
|
|
128
|
+
return { name, email };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Parse a repository reference: `owner/name`, `https://<host>/owner/name(.git)`,
|
|
132
|
+
* `git@<host>:owner/name(.git)`, or `ssh://git@<host>/owner/name(.git)`. The host must be
|
|
133
|
+
* github.com, or the GitHub Enterprise host behind `apiBaseUrl`.
|
|
134
|
+
* @param input - user text.
|
|
135
|
+
* @param apiBaseUrl - normalized API base URL.
|
|
136
|
+
* @returns owner and repository name.
|
|
137
|
+
* @throws Error explaining the accepted forms.
|
|
138
|
+
*/
|
|
139
|
+
export function parseRepository(input, apiBaseUrl) {
|
|
140
|
+
const text = input.trim();
|
|
141
|
+
const fail = (why) => {
|
|
142
|
+
throw new Error(`gh-pages-artifacts: "${input}" is not a repository (${why}); use owner/name or a remote URL like https://github.com/owner/name.git`);
|
|
143
|
+
};
|
|
144
|
+
let host;
|
|
145
|
+
let path;
|
|
146
|
+
const scp = /^[\w.-]+@([^:/\s]+):(.+)$/.exec(text);
|
|
147
|
+
if (scp !== null) {
|
|
148
|
+
host = scp[1].toLowerCase();
|
|
149
|
+
path = scp[2];
|
|
150
|
+
}
|
|
151
|
+
else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(text)) {
|
|
152
|
+
let url;
|
|
153
|
+
try {
|
|
154
|
+
url = new URL(text);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return fail('invalid URL');
|
|
158
|
+
}
|
|
159
|
+
host = url.hostname.toLowerCase();
|
|
160
|
+
path = url.pathname;
|
|
161
|
+
}
|
|
162
|
+
else if (/^[\w.-]+\.[a-z]{2,}\//i.test(text)) {
|
|
163
|
+
const slash = text.indexOf('/');
|
|
164
|
+
host = text.slice(0, slash).toLowerCase();
|
|
165
|
+
path = text.slice(slash);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
path = text;
|
|
169
|
+
}
|
|
170
|
+
if (host !== undefined) {
|
|
171
|
+
const apiHost = new URL(apiBaseUrl).hostname.toLowerCase();
|
|
172
|
+
const allowed = apiHost === 'api.github.com' ? ['github.com', 'www.github.com'] : [apiHost, apiHost.replace(/^api\./, '')];
|
|
173
|
+
if (!allowed.includes(host))
|
|
174
|
+
fail(`host ${host} does not match the configured GitHub (${allowed[0]})`);
|
|
175
|
+
}
|
|
176
|
+
const segments = path.replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '').split('/');
|
|
177
|
+
if (segments.length !== 2)
|
|
178
|
+
fail('expected exactly owner/name');
|
|
179
|
+
const [owner, repo] = segments;
|
|
180
|
+
if (!OWNER_PATTERN.test(owner))
|
|
181
|
+
fail(`invalid owner "${owner}"`);
|
|
182
|
+
if (!REPO_PATTERN.test(repo) || repo === '.' || repo === '..')
|
|
183
|
+
fail(`invalid repository name "${repo}"`);
|
|
184
|
+
return { owner, repo };
|
|
185
|
+
}
|
|
186
|
+
function normalizeRegistryDir(value) {
|
|
187
|
+
if (value === undefined || value.trim() === '') {
|
|
188
|
+
const home = process.env['DSH_HOME'] !== undefined && process.env['DSH_HOME'] !== '' ? process.env['DSH_HOME'] : join(homedir(), '.dsh');
|
|
189
|
+
return join(home, 'gh-pages-artifacts');
|
|
190
|
+
}
|
|
191
|
+
if (!isAbsolute(value))
|
|
192
|
+
throw new Error('gh-pages-artifacts: registryDir must be an absolute path');
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Normalize a relative directory option such as `a` or `pages/a` to slash-joined safe segments.
|
|
197
|
+
* @param value - raw option value.
|
|
198
|
+
* @param option - option name for error messages.
|
|
199
|
+
* @returns the normalized path, '' for the root.
|
|
200
|
+
*/
|
|
201
|
+
export function normalizeRelativeDir(value, option) {
|
|
202
|
+
const trimmed = value.trim().replace(/^\/+|\/+$/g, '');
|
|
203
|
+
if (trimmed === '')
|
|
204
|
+
return '';
|
|
205
|
+
const segments = trimmed.split('/');
|
|
206
|
+
for (const segment of segments) {
|
|
207
|
+
if (!SEGMENT_PATTERN.test(segment)) {
|
|
208
|
+
throw new Error(`gh-pages-artifacts: ${option} segment "${segment}" must start with a letter or digit and contain only letters, digits, '.', '_' or '-'`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return segments.join('/');
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Validate an http(s) URL option and strip its trailing slash.
|
|
215
|
+
* @param value - raw option value.
|
|
216
|
+
* @param option - option name for error messages.
|
|
217
|
+
* @param carriesToken - whether the token is sent to this URL, which then must be HTTPS unless loopback.
|
|
218
|
+
* @returns the URL without a trailing slash.
|
|
219
|
+
*/
|
|
220
|
+
export function normalizeUrl(value, option, carriesToken) {
|
|
221
|
+
let url;
|
|
222
|
+
try {
|
|
223
|
+
url = new URL(value.trim());
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
throw new Error(`gh-pages-artifacts: ${option} is not a valid URL: "${value}"`);
|
|
227
|
+
}
|
|
228
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
229
|
+
throw new Error(`gh-pages-artifacts: ${option} must be an http(s) URL`);
|
|
230
|
+
}
|
|
231
|
+
if (url.username !== '' || url.password !== '' || url.search !== '' || url.hash !== '') {
|
|
232
|
+
throw new Error(`gh-pages-artifacts: ${option} must not contain credentials, a query, or a fragment`);
|
|
233
|
+
}
|
|
234
|
+
if (carriesToken && url.protocol === 'http:' && !isLoopback(url.hostname)) {
|
|
235
|
+
throw new Error(`gh-pages-artifacts: ${option} must use https (plain http is allowed only for loopback test servers)`);
|
|
236
|
+
}
|
|
237
|
+
return url.href.replace(/\/+$/, '');
|
|
238
|
+
}
|
|
239
|
+
function isLoopback(hostname) {
|
|
240
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
|
|
241
|
+
}
|
package/lib/content.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Read publish sources from the Session workspace and screen text before it goes public. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import type { ToolRunContext } from '@deepseek-ai/dsh-tools';
|
|
4
|
+
/**
|
|
5
|
+
* Read one regular file that lies inside the Session working directory. Publishing makes data
|
|
6
|
+
* public, so this is stricter than the built-in read tool: symlinks, directories, and paths that
|
|
7
|
+
* resolve outside the workspace are refused.
|
|
8
|
+
* @param ctx - plugin context providing `fs`.
|
|
9
|
+
* @param exec - tool execution (Session working directory, cancellation).
|
|
10
|
+
* @param path - workspace-relative or absolute path.
|
|
11
|
+
* @param maxBytes - size ceiling.
|
|
12
|
+
* @returns the file bytes.
|
|
13
|
+
*/
|
|
14
|
+
export declare function readWorkspaceFile(ctx: Context, exec: ToolRunContext, path: string, maxBytes: number): Promise<Uint8Array>;
|
|
15
|
+
/**
|
|
16
|
+
* The last path segment, used as the default asset name.
|
|
17
|
+
* @param path - workspace path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function baseName(path: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* @param name - file name.
|
|
22
|
+
* @returns whether the name has a text extension.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isTextName(name: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Find credential-like strings in text that is about to be published.
|
|
27
|
+
* @param text - content to screen.
|
|
28
|
+
* @param knownSecrets - exact secret values that must never appear (for example the token in use);
|
|
29
|
+
* their base64, hex, and URL-encoded forms are checked too.
|
|
30
|
+
* @returns labels of the kinds found; empty when clean.
|
|
31
|
+
*/
|
|
32
|
+
export declare function findSecrets(text: string, knownSecrets?: readonly string[]): string[];
|
|
33
|
+
/**
|
|
34
|
+
* Check whether a workspace path looks like a credential file or a hidden file.
|
|
35
|
+
* @param path - workspace path as given.
|
|
36
|
+
* @returns a reason to refuse, or undefined when it looks like ordinary content.
|
|
37
|
+
*/
|
|
38
|
+
export declare function credentialPathProblem(path: string): string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* @param bytes - byte count.
|
|
41
|
+
* @returns a short human size.
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatBytes(bytes: number): string;
|