@distributionos/cli 0.1.15 → 0.1.17
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 +7 -7
- package/package.json +1 -1
- package/src/api.js +125 -125
- package/src/bootstrap.js +27 -25
- package/src/cli.js +387 -220
- package/src/constants.js +8 -8
- package/src/initialization.js +63 -63
- package/src/mcp-config.js +319 -187
- package/src/mutate.js +46 -43
- package/src/oauth.js +30 -30
- package/src/plan.js +175 -169
- package/src/setup-report.js +253 -222
- package/src/validation.js +108 -0
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# DistributionOS CLI
|
|
2
2
|
|
|
3
|
-
Installer for connecting an app repo to DistributionOS.
|
|
3
|
+
Installer for connecting an app repo to DistributionOS.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @distributionos/cli setup --app <appId>
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Default setup opens the DistributionOS MCP OAuth approval flow when needed, then prints a setup review first. In an interactive terminal, approve the plan to apply the managed setup changes. In non-interactive agent runs, use `--apply` only after reviewing the setup output.
|
|
9
|
+
Default setup opens the DistributionOS MCP OAuth approval flow when needed, then prints a setup review first. In an interactive terminal, approve the plan to apply the managed setup changes. In non-interactive agent runs, use `--apply` only after reviewing the setup output.
|
|
10
10
|
|
|
11
|
-
Agents that support installable skills can also load the public DistributionOS agent skill before using the CLI:
|
|
12
|
-
|
|
13
|
-
https://github.com/lawfan1026/distributionos-agent-skill
|
|
14
|
-
|
|
15
|
-
The CLI remains the default setup path. The skill is a public agent-readable guide for when to use the CLI, MCP, API fallback, analytics install, verification, and implementation reporting.
|
|
11
|
+
Agents that support installable skills can also load the public DistributionOS agent skill before using the CLI:
|
|
12
|
+
|
|
13
|
+
https://github.com/lawfan1026/distributionos-agent-skill
|
|
14
|
+
|
|
15
|
+
The CLI remains the default setup path. The skill is a public agent-readable guide for when to use the CLI, MCP, API fallback, analytics install, verification, and implementation reporting.
|
|
16
16
|
|
|
17
17
|
## Commands
|
|
18
18
|
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { DEFAULT_API_BASE, TOKEN_ENV_NAMES } from './constants.js';
|
|
2
|
-
import { getStoredCredential } from './auth-store.js';
|
|
3
|
-
import { isCredentialFresh, refreshOAuthCredential } from './oauth.js';
|
|
4
|
-
|
|
5
|
-
export class DistributionOsApiError extends Error {
|
|
6
|
-
constructor(message, { status = 0, code = null } = {}) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = 'DistributionOsApiError';
|
|
9
|
-
this.status = status;
|
|
10
|
-
this.code = code;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function resolveEnvToken(env) {
|
|
15
|
-
for (const name of TOKEN_ENV_NAMES) {
|
|
16
|
-
const value = env?.[name]?.trim();
|
|
17
|
-
if (value) return { source: 'env', name, value };
|
|
18
|
-
}
|
|
2
|
+
import { getStoredCredential } from './auth-store.js';
|
|
3
|
+
import { isCredentialFresh, refreshOAuthCredential } from './oauth.js';
|
|
4
|
+
|
|
5
|
+
export class DistributionOsApiError extends Error {
|
|
6
|
+
constructor(message, { status = 0, code = null } = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'DistributionOsApiError';
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveEnvToken(env) {
|
|
15
|
+
for (const name of TOKEN_ENV_NAMES) {
|
|
16
|
+
const value = env?.[name]?.trim();
|
|
17
|
+
if (value) return { source: 'env', name, value };
|
|
18
|
+
}
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -42,111 +42,111 @@ export async function resolveAuthToken({ env, apiBase, appId, fetchImpl }) {
|
|
|
42
42
|
: null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export async function fetchDistributionOsSetupContext(input) {
|
|
45
|
+
export async function fetchDistributionOsSetupContext(input) {
|
|
46
|
+
const token = await resolveAuthToken({
|
|
47
|
+
env: input.env,
|
|
48
|
+
apiBase: input.apiBase ?? DEFAULT_API_BASE,
|
|
49
|
+
appId: input.appId,
|
|
50
|
+
fetchImpl: input.fetch,
|
|
51
|
+
});
|
|
52
|
+
if (!token || input.noFetch) {
|
|
53
|
+
return {
|
|
54
|
+
status: input.noFetch ? 'skipped' : 'not_authenticated',
|
|
55
|
+
tokenName: token?.name ?? null,
|
|
56
|
+
access: {
|
|
57
|
+
allowed: false,
|
|
58
|
+
reason: input.noFetch ? 'remote_fetch_skipped' : 'not_authenticated',
|
|
59
|
+
billingUrl: `${normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE)}/dashboard/account/billing`,
|
|
60
|
+
},
|
|
61
|
+
usage: null,
|
|
62
|
+
instructions: null,
|
|
63
|
+
analyticsContract: null,
|
|
64
|
+
setupContract: null,
|
|
65
|
+
errors: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
|
|
70
|
+
const errors = [];
|
|
71
|
+
let subscriptionRequired = false;
|
|
72
|
+
const capture = (label) => (error) => {
|
|
73
|
+
if (error instanceof DistributionOsApiError && (error.status === 402 || error.code === 'SUBSCRIPTION_REQUIRED')) {
|
|
74
|
+
subscriptionRequired = true;
|
|
75
|
+
}
|
|
76
|
+
errors.push(`${label} failed: ${error.message}`);
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const [instructions, analyticsContract, usage, setupContract] = await Promise.all([
|
|
81
|
+
fetchJson({
|
|
82
|
+
fetchImpl: input.fetch,
|
|
83
|
+
apiBase,
|
|
84
|
+
token: token.value,
|
|
85
|
+
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/agent/instructions`,
|
|
86
|
+
}).catch(capture('Agent instructions fetch')),
|
|
87
|
+
fetchJson({
|
|
88
|
+
fetchImpl: input.fetch,
|
|
89
|
+
apiBase,
|
|
90
|
+
token: token.value,
|
|
91
|
+
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/analytics/contract`,
|
|
92
|
+
}).catch(capture('Analytics contract fetch')),
|
|
93
|
+
fetchJson({
|
|
94
|
+
fetchImpl: input.fetch,
|
|
95
|
+
apiBase,
|
|
96
|
+
token: token.value,
|
|
97
|
+
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/usage`,
|
|
98
|
+
}).catch(capture('Usage fetch')),
|
|
99
|
+
fetchJson({
|
|
100
|
+
fetchImpl: input.fetch,
|
|
101
|
+
apiBase,
|
|
102
|
+
token: token.value,
|
|
103
|
+
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/setup/contract`,
|
|
104
|
+
}).catch(capture('Setup contract fetch')),
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
status: errors.length === 0 ? 'fetched' : 'partial',
|
|
109
|
+
tokenName: token.name,
|
|
110
|
+
access: {
|
|
111
|
+
allowed: !subscriptionRequired,
|
|
112
|
+
reason: subscriptionRequired ? 'subscription_required' : errors.length === 0 ? 'active' : 'partial',
|
|
113
|
+
billingUrl: `${apiBase}/dashboard/account/billing`,
|
|
114
|
+
},
|
|
115
|
+
usage,
|
|
116
|
+
instructions,
|
|
117
|
+
analyticsContract,
|
|
118
|
+
setupContract,
|
|
119
|
+
errors,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function reportSetupInstallation(input) {
|
|
46
124
|
const token = await resolveAuthToken({
|
|
47
125
|
env: input.env,
|
|
48
126
|
apiBase: input.apiBase ?? DEFAULT_API_BASE,
|
|
49
127
|
appId: input.appId,
|
|
50
128
|
fetchImpl: input.fetch,
|
|
51
|
-
});
|
|
52
|
-
if (!token
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const capture = (label) => (error) => {
|
|
73
|
-
if (error instanceof DistributionOsApiError && (error.status === 402 || error.code === 'SUBSCRIPTION_REQUIRED')) {
|
|
74
|
-
subscriptionRequired = true;
|
|
75
|
-
}
|
|
76
|
-
errors.push(`${label} failed: ${error.message}`);
|
|
77
|
-
return null;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const [instructions, analyticsContract, usage, setupContract] = await Promise.all([
|
|
81
|
-
fetchJson({
|
|
82
|
-
fetchImpl: input.fetch,
|
|
83
|
-
apiBase,
|
|
84
|
-
token: token.value,
|
|
85
|
-
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/agent/instructions`,
|
|
86
|
-
}).catch(capture('Agent instructions fetch')),
|
|
87
|
-
fetchJson({
|
|
88
|
-
fetchImpl: input.fetch,
|
|
89
|
-
apiBase,
|
|
90
|
-
token: token.value,
|
|
91
|
-
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/analytics/contract`,
|
|
92
|
-
}).catch(capture('Analytics contract fetch')),
|
|
93
|
-
fetchJson({
|
|
94
|
-
fetchImpl: input.fetch,
|
|
95
|
-
apiBase,
|
|
96
|
-
token: token.value,
|
|
97
|
-
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/usage`,
|
|
98
|
-
}).catch(capture('Usage fetch')),
|
|
99
|
-
fetchJson({
|
|
100
|
-
fetchImpl: input.fetch,
|
|
101
|
-
apiBase,
|
|
102
|
-
token: token.value,
|
|
103
|
-
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/setup/contract`,
|
|
104
|
-
}).catch(capture('Setup contract fetch')),
|
|
105
|
-
]);
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
status: errors.length === 0 ? 'fetched' : 'partial',
|
|
109
|
-
tokenName: token.name,
|
|
110
|
-
access: {
|
|
111
|
-
allowed: !subscriptionRequired,
|
|
112
|
-
reason: subscriptionRequired ? 'subscription_required' : errors.length === 0 ? 'active' : 'partial',
|
|
113
|
-
billingUrl: `${apiBase}/dashboard/account/billing`,
|
|
114
|
-
},
|
|
115
|
-
usage,
|
|
116
|
-
instructions,
|
|
117
|
-
analyticsContract,
|
|
118
|
-
setupContract,
|
|
119
|
-
errors,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export async function reportSetupInstallation(input) {
|
|
124
|
-
const token = await resolveAuthToken({
|
|
125
|
-
env: input.env,
|
|
126
|
-
apiBase: input.apiBase ?? DEFAULT_API_BASE,
|
|
127
|
-
appId: input.appId,
|
|
128
|
-
fetchImpl: input.fetch,
|
|
129
|
-
});
|
|
130
|
-
if (!token) {
|
|
131
|
-
throw new Error('No stored OAuth token or env token was available.');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
|
|
135
|
-
const response = await input.fetch(`${apiBase}/api/v1/apps/${encodeURIComponent(input.appId)}/setup/report`, {
|
|
136
|
-
method: 'POST',
|
|
137
|
-
headers: {
|
|
138
|
-
Authorization: `Bearer ${token.value}`,
|
|
139
|
-
'Content-Type': 'application/json',
|
|
140
|
-
Accept: 'application/json',
|
|
141
|
-
},
|
|
142
|
-
body: JSON.stringify(input.report),
|
|
143
|
-
});
|
|
144
|
-
const body = await response.json().catch(() => null);
|
|
145
|
-
if (!response.ok || body?.success === false) {
|
|
146
|
-
throw new Error(body?.error || `Setup report failed with HTTP ${response.status}`);
|
|
147
|
-
}
|
|
148
|
-
return body?.data ?? body;
|
|
149
|
-
}
|
|
129
|
+
});
|
|
130
|
+
if (!token) {
|
|
131
|
+
throw new Error('No stored OAuth token or env token was available.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
|
|
135
|
+
const response = await input.fetch(`${apiBase}/api/v1/apps/${encodeURIComponent(input.appId)}/setup/report`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
Authorization: `Bearer ${token.value}`,
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
Accept: 'application/json',
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify(input.report),
|
|
143
|
+
});
|
|
144
|
+
const body = await response.json().catch(() => null);
|
|
145
|
+
if (!response.ok || body?.success === false) {
|
|
146
|
+
throw new Error(body?.error || `Setup report failed with HTTP ${response.status}`);
|
|
147
|
+
}
|
|
148
|
+
return body?.data ?? body;
|
|
149
|
+
}
|
|
150
150
|
|
|
151
151
|
export async function ensureAnalyticsSite(input) {
|
|
152
152
|
const token = await resolveAuthToken({
|
|
@@ -264,14 +264,14 @@ async function fetchJson({ fetchImpl, apiBase, token, path }) {
|
|
|
264
264
|
},
|
|
265
265
|
});
|
|
266
266
|
const body = await response.json().catch(() => null);
|
|
267
|
-
if (!response.ok || body?.success === false) {
|
|
268
|
-
throw new DistributionOsApiError(body?.error || `HTTP ${response.status}`, {
|
|
269
|
-
status: response.status,
|
|
270
|
-
code: body?.code ?? null,
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
return body?.data ?? body;
|
|
274
|
-
}
|
|
267
|
+
if (!response.ok || body?.success === false) {
|
|
268
|
+
throw new DistributionOsApiError(body?.error || `HTTP ${response.status}`, {
|
|
269
|
+
status: response.status,
|
|
270
|
+
code: body?.code ?? null,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return body?.data ?? body;
|
|
274
|
+
}
|
|
275
275
|
|
|
276
276
|
function normalizeApiBase(value) {
|
|
277
277
|
return String(value || DEFAULT_API_BASE).replace(/\/+$/, '');
|
package/src/bootstrap.js
CHANGED
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
const BOOTSTRAP_BLOCK_RE =
|
|
8
8
|
/<!--\s*DISTRIBUTIONOS:START[\s\S]*?DISTRIBUTIONOS:END\s*-->/i;
|
|
9
9
|
|
|
10
|
-
export function buildSetupCommand(appId, packageName = CLI_PACKAGE_NAME) {
|
|
11
|
-
const packageSpec = packageName === CLI_PACKAGE_NAME ? `${packageName}@latest` : packageName;
|
|
12
|
-
return `npx --yes ${packageSpec} setup --app ${shellToken(appId)}`;
|
|
13
|
-
}
|
|
10
|
+
export function buildSetupCommand(appId, packageName = CLI_PACKAGE_NAME) {
|
|
11
|
+
const packageSpec = packageName === CLI_PACKAGE_NAME ? `${packageName}@latest` : packageName;
|
|
12
|
+
return `npx --yes ${packageSpec} setup --app ${shellToken(appId)}`;
|
|
13
|
+
}
|
|
14
14
|
|
|
15
15
|
export function buildLocalBootstrapBlock(version = CURRENT_BOOTSTRAP_VERSION, appId) {
|
|
16
16
|
const appIdAttribute = appId ? ` appId=${appId}` : '';
|
|
@@ -24,25 +24,27 @@ export function buildLocalBootstrapBlock(version = CURRENT_BOOTSTRAP_VERSION, ap
|
|
|
24
24
|
'',
|
|
25
25
|
'This repo is linked to DistributionOS.',
|
|
26
26
|
'',
|
|
27
|
-
'Hard preflight: before editing files, creating assets, generating images, or changing copy for SEO, blog, launch, content, visual asset, competitor, social, email, or growth work, fetch the current instructions from DistributionOS:',
|
|
27
|
+
'Hard preflight: before editing files, creating assets, generating images, creating HyperFrames launch/demo videos, or changing copy for SEO, blog, launch, content, visual asset, competitor, social, email, or growth work, fetch the current instructions from DistributionOS:',
|
|
28
|
+
'',
|
|
29
|
+
instructionCall,
|
|
30
|
+
'',
|
|
31
|
+
'This preflight applies even when the user does not mention DistributionOS by name. Do not start from local repo files alone for those tasks.',
|
|
32
|
+
'',
|
|
33
|
+
'If the user asks about DistributionOS analytics, context, setup state, or connection checks and DistributionOS tools are not visible, do not inspect unrelated dashboards or repos. Tell the user to restart or reconnect the agent session from this app repo and approve the DistributionOS MCP server.',
|
|
28
34
|
'',
|
|
29
|
-
|
|
30
|
-
'',
|
|
31
|
-
'
|
|
32
|
-
'',
|
|
33
|
-
'
|
|
34
|
-
'',
|
|
35
|
-
'
|
|
36
|
-
'',
|
|
37
|
-
'If MCP remains unavailable, ask before using an API-key fallback. Never write API keys into tracked files, repo instructions, committed MCP config, README, or logs.',
|
|
38
|
-
'',
|
|
39
|
-
'For existing blog posts or DistributionOS artifacts, fetch the artifact/context first. Use DistributionOS image/media workflows before local image tools.',
|
|
35
|
+
'If only DistributionOS authentication tools are visible, authenticate first, wait for the user to approve OAuth in the browser, complete authentication, then call check_distributionos_connection, get_agent_instructions, and the task-specific tool such as get_analytics_summary.',
|
|
36
|
+
'',
|
|
37
|
+
'If MCP remains unavailable, ask before using an API-key fallback. Never write API keys into tracked files, repo instructions, committed MCP config, README, or logs.',
|
|
38
|
+
'',
|
|
39
|
+
'For existing blog posts or DistributionOS artifacts, fetch the artifact/context first. Use DistributionOS image/media workflows before local image tools.',
|
|
40
|
+
'',
|
|
41
|
+
'For HyperFrames launch/demo/feature video work, use DistributionOS to decide what video is worth making: fetch app context, Brain Doc/context pack, Opportunity Map, and existing artifacts; create or fetch a video_brief with safe claims, CTA, tracking plan, starter frame.md, storyboard, asset checklist, and HyperFrames handoff. Then use the local HyperFrames skill or CLI for composition/rendering, and report the final render/URL back as a linked video_asset.',
|
|
40
42
|
'',
|
|
41
43
|
'When you create a plan, strategy, roadmap, campaign, content ideas, keyword priorities, or next-step recommendations, save it to DistributionOS as proposed work for review before executing.',
|
|
42
44
|
'',
|
|
43
45
|
'When you publish, ship, schedule, or implement a DistributionOS artifact, report the implementation back to DistributionOS before your final response.',
|
|
44
46
|
'',
|
|
45
|
-
'If analytics is enabled, public blog posts, landing pages, CTAs, email links, social links, launch links, paid links, and campaigns must use the DistributionOS analytics contract by default: call get_analytics_install_instructions, add page/link markers, use create_tracked_link for campaign links, verify_analytics_install after deploy, or report a specific analyticsOptOutReason.',
|
|
47
|
+
'If analytics is enabled, public blog posts, landing pages, CTAs, email links, social links, launch links, paid links, and campaigns must use the DistributionOS analytics contract by default: call get_analytics_install_instructions, add page/link markers, use create_tracked_link for campaign links, wire public checkout_started/signup/waitlist/email success events where safe, verify_analytics_install after deploy, or report a specific analyticsOptOutReason.',
|
|
46
48
|
'',
|
|
47
49
|
'For SEO blog/page publishes, verify 200, canonical, sitemap inclusion, robots/indexability, one H1, meta description, and schema before reporting.',
|
|
48
50
|
'',
|
|
@@ -100,14 +102,14 @@ export function getBootstrapAction(instructionFiles, recommendedBlock) {
|
|
|
100
102
|
|
|
101
103
|
export function buildFallbackMcpInstructions(appId) {
|
|
102
104
|
return [
|
|
103
|
-
`App ID: ${appId}`,
|
|
104
|
-
`DistributionOS MCP server: ${MCP_SERVER_URL}`,
|
|
105
|
-
'Use OAuth sign-in when prompted, then call get_agent_instructions and submit_initialization_context.',
|
|
106
|
-
'If only DistributionOS authentication tools appear, authenticate first, wait for the user to approve OAuth, then complete authentication before calling any other DistributionOS tool.',
|
|
107
|
-
'If OAuth is unavailable, create an app-scoped write API key in DistributionOS and expose it only as DISTRIBUTIONOS_API_KEY for the command invocation.',
|
|
108
|
-
'Never write the API key into AGENTS.md, CLAUDE.md, README, committed MCP config, or any tracked file.',
|
|
109
|
-
];
|
|
110
|
-
}
|
|
105
|
+
`App ID: ${appId}`,
|
|
106
|
+
`DistributionOS MCP server: ${MCP_SERVER_URL}`,
|
|
107
|
+
'Use OAuth sign-in when prompted, then call get_agent_instructions and submit_initialization_context.',
|
|
108
|
+
'If only DistributionOS authentication tools appear, authenticate first, wait for the user to approve OAuth, then complete authentication before calling any other DistributionOS tool.',
|
|
109
|
+
'If OAuth is unavailable, create an app-scoped write API key in DistributionOS and expose it only as DISTRIBUTIONOS_API_KEY for the command invocation.',
|
|
110
|
+
'Never write the API key into AGENTS.md, CLAUDE.md, README, committed MCP config, or any tracked file.',
|
|
111
|
+
];
|
|
112
|
+
}
|
|
111
113
|
|
|
112
114
|
function shellToken(value) {
|
|
113
115
|
if (/^[a-zA-Z0-9._:-]+$/.test(value)) return value;
|